diff --git a/codex-rs/core/src/memories/storage.rs b/codex-rs/core/src/memories/storage.rs index 1a07dbe9b..bd3eaa23a 100644 --- a/codex-rs/core/src/memories/storage.rs +++ b/codex-rs/core/src/memories/storage.rs @@ -82,6 +82,8 @@ async fn rebuild_raw_memories_file( ) .map_err(raw_memories_format_error)?; writeln!(body, "cwd: {}", memory.cwd.display()).map_err(raw_memories_format_error)?; + writeln!(body, "rollout_path: {}", memory.rollout_path.display()) + .map_err(raw_memories_format_error)?; let rollout_summary_file = format!("{}.md", rollout_summary_file_stem(memory)); writeln!(body, "rollout_summary_file: {rollout_summary_file}") .map_err(raw_memories_format_error)?; @@ -138,6 +140,8 @@ async fn write_rollout_summary_for_thread( memory.source_updated_at.to_rfc3339() ) .map_err(rollout_summary_format_error)?; + writeln!(body, "rollout_path: {}", memory.rollout_path.display()) + .map_err(rollout_summary_format_error)?; writeln!(body, "cwd: {}", memory.cwd.display()).map_err(rollout_summary_format_error)?; writeln!(body).map_err(rollout_summary_format_error)?; body.push_str(&memory.rollout_summary); @@ -267,6 +271,7 @@ mod tests { raw_memory: "raw memory".to_string(), rollout_summary: "summary".to_string(), rollout_slug: rollout_slug.map(ToString::to_string), + rollout_path: PathBuf::from("/tmp/rollout.jsonl"), cwd: PathBuf::from("/tmp/workspace"), generated_at: Utc.timestamp_opt(124, 0).single().expect("timestamp"), } diff --git a/codex-rs/core/src/memories/tests.rs b/codex-rs/core/src/memories/tests.rs index b47e7f8aa..8f5fd5a81 100644 --- a/codex-rs/core/src/memories/tests.rs +++ b/codex-rs/core/src/memories/tests.rs @@ -86,6 +86,7 @@ async fn sync_rollout_summaries_and_raw_memories_file_keeps_latest_memories_only raw_memory: "raw memory".to_string(), rollout_summary: "short summary".to_string(), rollout_slug: None, + rollout_path: PathBuf::from("/tmp/rollout-100.jsonl"), cwd: PathBuf::from("/tmp/workspace"), generated_at: Utc.timestamp_opt(101, 0).single().expect("timestamp"), }]; @@ -135,6 +136,7 @@ async fn sync_rollout_summaries_and_raw_memories_file_keeps_latest_memories_only assert!(raw_memories.contains("raw memory")); assert!(raw_memories.contains(&keep_id)); assert!(raw_memories.contains("cwd: /tmp/workspace")); + assert!(raw_memories.contains("rollout_path: /tmp/rollout-100.jsonl")); assert!(raw_memories.contains(&format!( "rollout_summary_file: {canonical_rollout_summary_file}" ))); @@ -150,6 +152,10 @@ async fn sync_rollout_summaries_and_raw_memories_file_keeps_latest_memories_only .find("cwd: /tmp/workspace") .map(|offset| thread_pos + offset) .expect("cwd should exist after thread header"); + let rollout_path_pos = raw_memories[thread_pos..] + .find("rollout_path: /tmp/rollout-100.jsonl") + .map(|offset| thread_pos + offset) + .expect("rollout_path should exist after thread header"); let file_pos = raw_memories[thread_pos..] .find(&format!( "rollout_summary_file: {canonical_rollout_summary_file}" @@ -158,7 +164,8 @@ async fn sync_rollout_summaries_and_raw_memories_file_keeps_latest_memories_only .expect("rollout_summary_file should exist after thread header"); assert!(thread_pos < updated_pos); assert!(updated_pos < cwd_pos); - assert!(cwd_pos < file_pos); + assert!(cwd_pos < rollout_path_pos); + assert!(rollout_path_pos < file_pos); } #[tokio::test] @@ -184,6 +191,7 @@ async fn sync_rollout_summaries_uses_timestamp_hash_and_sanitized_slug_filename( raw_memory: "raw memory".to_string(), rollout_summary: "short summary".to_string(), rollout_slug: Some("Unsafe Slug/With Spaces & Symbols + EXTRA_LONG_12345".to_string()), + rollout_path: PathBuf::from("/tmp/rollout-200.jsonl"), cwd: PathBuf::from("/tmp/workspace"), generated_at: Utc.timestamp_opt(201, 0).single().expect("timestamp"), }]; @@ -239,6 +247,7 @@ async fn sync_rollout_summaries_uses_timestamp_hash_and_sanitized_slug_filename( .await .expect("read rollout summary"); assert!(summary.contains(&format!("thread_id: {thread_id}"))); + assert!(summary.contains("rollout_path: /tmp/rollout-200.jsonl")); assert!( !tokio::fs::try_exists(&stale_unslugged_path) .await @@ -283,6 +292,7 @@ task_outcome: success .to_string(), rollout_summary: "short summary".to_string(), rollout_slug: Some("Unsafe Slug/With Spaces & Symbols + EXTRA_LONG_12345".to_string()), + rollout_path: PathBuf::from("/tmp/rollout-200.jsonl"), cwd: PathBuf::from("/tmp/workspace"), generated_at: Utc.timestamp_opt(201, 0).single().expect("timestamp"), }]; @@ -316,6 +326,12 @@ task_outcome: success let raw_memories = tokio::fs::read_to_string(raw_memories_file(&root)) .await .expect("read raw memories"); + let summary = tokio::fs::read_to_string( + rollout_summaries_dir(&root).join(canonical_rollout_summary_file), + ) + .await + .expect("read rollout summary"); + assert!(summary.contains("rollout_path: /tmp/rollout-200.jsonl")); assert!(raw_memories.contains(&format!( "rollout_summary_file: {canonical_rollout_summary_file}" ))); @@ -360,6 +376,7 @@ mod phase2 { raw_memory: "raw memory".to_string(), rollout_summary: "rollout summary".to_string(), rollout_slug: None, + rollout_path: PathBuf::from("/tmp/rollout-summary.jsonl"), cwd: PathBuf::from("/tmp/workspace"), generated_at: chrono::DateTime::::from_timestamp(source_updated_at + 1, 0) .expect("valid generated_at timestamp"), diff --git a/codex-rs/core/templates/memories/consolidation.md b/codex-rs/core/templates/memories/consolidation.md index b880bdfd6..000fddedf 100644 --- a/codex-rs/core/templates/memories/consolidation.md +++ b/codex-rs/core/templates/memories/consolidation.md @@ -106,7 +106,7 @@ Under `{{ memory_root }}/`: context. - source of rollout-level metadata needed for MEMORY.md `### rollout_summary_files` annotations; - you should be able to find `cwd` and `updated_at` there. + you should be able to find `cwd`, `rollout_path`, and `updated_at` there. - `MEMORY.md` - merged memories; produce a lightly clustered version if applicable - `rollout_summaries/*.md` @@ -171,7 +171,7 @@ Required task-oriented body shape (strict): ## Task 1: ### rollout_summary_files -- (cwd=, updated_at=, thread_id=, ) +- (cwd=, rollout_path=, updated_at=, thread_id=, ) ### keywords @@ -236,7 +236,8 @@ Schema rules (strict): - Every `## Task ` section must include `### rollout_summary_files`, `### keywords`, and `### learnings`. - `### rollout_summary_files` must be task-local (not a block-wide catch-all list). - - Each rollout annotation must include `cwd=` and `updated_at=`. + - Each rollout annotation must include `cwd=`, `rollout_path=`, and + `updated_at=`. If missing from a rollout summary, recover them from `raw_memories.md`. - Major learnings should be traceable to rollout summaries listed in the same task section. - Order rollout references by freshness and practical usefulness. diff --git a/codex-rs/core/templates/memories/read_path.md b/codex-rs/core/templates/memories/read_path.md index 59159b2ec..ba0e21044 100644 --- a/codex-rs/core/templates/memories/read_path.md +++ b/codex-rs/core/templates/memories/read_path.md @@ -25,7 +25,10 @@ Memory layout (general -> specific): - scripts/ (optional helper scripts) - examples/ (optional example outputs) - templates/ (optional templates) -- {{ base_path }}/rollout_summaries/ (per-rollout recaps + evidence snippets) + - {{ base_path }}/rollout_summaries/ (per-rollout recaps + evidence snippets) + - The paths of these entries can be found in {{ base_path }}/MEMORY.md or {{ base_path }}/rollout_summaries/ as `rollout_path` + - These files are append-only `jsonl`: `session_meta.payload.id` identifies the session, `turn_context` marks turn boundaries, `event_msg` is the lightweight status stream, and `response_item` contains actual messages, tool calls, and tool outputs. + - For efficient lookup, prefer matching the filename suffix or `session_meta.payload.id`; avoid broad full-content scans unless needed. Quick memory pass (when applicable): @@ -34,7 +37,8 @@ Quick memory pass (when applicable): 3. Only if MEMORY.md directly points to rollout summaries/skills, open the 1-2 most relevant files under {{ base_path }}/rollout_summaries/ or {{ base_path }}/skills/. -4. If there are no relevant hits, stop memory lookup and continue normally. +4. If above are not clear and you need exact commands, error text, or precise evidence, search over `rollout_path` for more evidence. +5. If there are no relevant hits, stop memory lookup and continue normally. Quick-pass budget: diff --git a/codex-rs/state/src/model/memories.rs b/codex-rs/state/src/model/memories.rs index 813c99939..fc2468d83 100644 --- a/codex-rs/state/src/model/memories.rs +++ b/codex-rs/state/src/model/memories.rs @@ -12,6 +12,7 @@ use super::ThreadMetadata; #[derive(Debug, Clone, PartialEq, Eq)] pub struct Stage1Output { pub thread_id: ThreadId, + pub rollout_path: PathBuf, pub source_updated_at: DateTime, pub raw_memory: String, pub rollout_summary: String, @@ -23,6 +24,7 @@ pub struct Stage1Output { #[derive(Debug)] pub(crate) struct Stage1OutputRow { thread_id: String, + rollout_path: String, source_updated_at: i64, raw_memory: String, rollout_summary: String, @@ -35,6 +37,7 @@ impl Stage1OutputRow { pub(crate) fn try_from_row(row: &SqliteRow) -> Result { Ok(Self { thread_id: row.try_get("thread_id")?, + rollout_path: row.try_get("rollout_path")?, source_updated_at: row.try_get("source_updated_at")?, raw_memory: row.try_get("raw_memory")?, rollout_summary: row.try_get("rollout_summary")?, @@ -51,6 +54,7 @@ impl TryFrom for Stage1Output { fn try_from(row: Stage1OutputRow) -> std::result::Result { Ok(Self { thread_id: ThreadId::try_from(row.thread_id)?, + rollout_path: PathBuf::from(row.rollout_path), source_updated_at: epoch_seconds_to_datetime(row.source_updated_at)?, raw_memory: row.raw_memory, rollout_summary: row.rollout_summary, diff --git a/codex-rs/state/src/runtime/memories.rs b/codex-rs/state/src/runtime/memories.rs index 6d5ffc888..c6c95aa1c 100644 --- a/codex-rs/state/src/runtime/memories.rs +++ b/codex-rs/state/src/runtime/memories.rs @@ -218,7 +218,7 @@ LEFT JOIN jobs /// /// Query behavior: /// - filters out rows where both `raw_memory` and `rollout_summary` are blank - /// - joins `threads` to include thread `cwd` + /// - joins `threads` to include thread `cwd` and `rollout_path` /// - orders by `source_updated_at DESC, thread_id DESC` /// - applies `LIMIT n` pub async fn list_stage1_outputs_for_global( @@ -233,6 +233,7 @@ LEFT JOIN jobs r#" SELECT so.thread_id, + COALESCE(t.rollout_path, '') AS rollout_path, so.source_updated_at, so.raw_memory, so.rollout_summary,