Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion codex-rs/core/src/session/multi_agents.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,6 @@ pub(super) fn usage_hint_text<'a>(
| SessionSource::Mcp
| SessionSource::Custom(_)
| SessionSource::Unknown => multi_agent_v2.root_agent_usage_hint_text.as_deref(),
SessionSource::SubAgent(_) => None,
SessionSource::Internal(_) | SessionSource::SubAgent(_) => None,
}
}
9 changes: 5 additions & 4 deletions codex-rs/memories/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ What it does:
`last_usage` / `generated_at`
- computes a completion watermark from the claimed watermark + newest input timestamps
- syncs local memory artifacts under the memories root:
- `raw_memories.md` (merged raw memories, latest first)
- `raw_memories.md` (merged raw memories, stable ascending thread-id order)
- `rollout_summaries/` (one summary file per selected rollout)
- keeps the memories root itself as a git-baseline directory, initialized under
`~/.codex/memories/.git` by `codex-git-utils`
Expand Down Expand Up @@ -127,9 +127,10 @@ Selection and workspace-diff behavior:
- Phase 1 upserts preserve the previous `selected_for_phase2` baseline until
the next successful Phase 2 run rewrites it
- Phase 2 loads only the current top-N selected stage-1 inputs, syncs
`rollout_summaries/` and `raw_memories.md` directly to that selection, then
lets the git-style workspace diff surface additions, modifications, and
deletions against the previous successful memory baseline
`rollout_summaries/` directly to that selection, renders `raw_memories.md`
in stable ascending thread-id order to avoid usage-rank churn, then lets the
git-style workspace diff surface additions, modifications, and deletions
against the previous successful memory baseline
- when the selected input set is empty, stale `rollout_summaries/` files are
removed and `raw_memories.md` is rewritten to the empty-input placeholder;
consolidated outputs such as `MEMORY.md`, `memory_summary.md`, and `skills/`
Expand Down
2 changes: 1 addition & 1 deletion codex-rs/memories/write/src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ async fn rebuild_raw_memories_file(
return tokio::fs::write(raw_memories_file(root), body).await;
}

body.push_str("Merged stage-1 raw memories (latest first):\n\n");
body.push_str("Merged stage-1 raw memories (stable ascending thread-id order):\n\n");
for memory in retained {
writeln!(body, "## Thread `{}`", memory.thread_id).map_err(raw_memories_format_error)?;
writeln!(
Expand Down
11 changes: 6 additions & 5 deletions codex-rs/memories/write/templates/memories/consolidation.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,11 +120,12 @@ Primary inputs (always read these, if exists):
Under `{{ memory_root }}/`:

- `raw_memories.md`
- mechanical merge of `raw_memories` from Phase 1; ordered latest-first.
- Use this recency ordering as a major heuristic when choosing what to promote, expand, or deprecate.
- Default scan order: top-to-bottom. In INCREMENTAL UPDATE mode, bias attention toward the newest
portion first, then expand to older entries with enough coverage to avoid missing important older
context.
- mechanical merge of selected `raw_memories` from Phase 1; ordered by stable ascending thread id.
- Do not treat file order as recency or importance; use `updated_at`, workspace diff context,
and rollout content when choosing what to promote, expand, or deprecate.
- Default scan order: top-to-bottom. In INCREMENTAL UPDATE mode, use the workspace diff to find
changed entries first, then expand to unchanged entries with enough coverage to avoid missing
important older context.
- source of rollout-level metadata needed for MEMORY.md `### rollout_summary_files`
annotations;
you should be able to find `cwd`, `rollout_path`, and `updated_at` there.
Expand Down
117 changes: 72 additions & 45 deletions codex-rs/state/src/runtime/memories.rs
Original file line number Diff line number Diff line change
Expand Up @@ -335,9 +335,10 @@ WHERE thread_id IN (
/// `last_usage` is within `max_unused_days`, or whose
/// `source_updated_at` is within that window when the memory has never
/// been used
/// - eligible rows are ordered by `usage_count DESC`,
/// - eligible rows are ranked by `usage_count DESC`,
/// `COALESCE(last_usage, source_updated_at) DESC`, `source_updated_at DESC`,
/// `thread_id DESC`
/// - the selected top-N rows are returned in stable `thread_id ASC` order
///
/// The returned rows are the complete Phase 2 filesystem input. Phase 2
/// syncs these rows directly; deletions are represented by the workspace
Expand All @@ -355,30 +356,43 @@ WHERE thread_id IN (
let current_rows = sqlx::query(
r#"
SELECT
so.thread_id,
COALESCE(t.rollout_path, '') AS rollout_path,
so.source_updated_at,
so.raw_memory,
so.rollout_summary,
so.rollout_slug,
so.generated_at,
COALESCE(t.cwd, '') AS cwd,
t.git_branch AS git_branch
FROM stage1_outputs AS so
LEFT JOIN threads AS t
ON t.id = so.thread_id
WHERE t.memory_mode = 'enabled'
AND (length(trim(so.raw_memory)) > 0 OR length(trim(so.rollout_summary)) > 0)
AND (
(so.last_usage IS NOT NULL AND so.last_usage >= ?)
OR (so.last_usage IS NULL AND so.source_updated_at >= ?)
)
ORDER BY
COALESCE(so.usage_count, 0) DESC,
COALESCE(so.last_usage, so.source_updated_at) DESC,
so.source_updated_at DESC,
so.thread_id DESC
LIMIT ?
selected.thread_id,
selected.rollout_path,
selected.source_updated_at,
selected.raw_memory,
selected.rollout_summary,
selected.rollout_slug,
selected.generated_at,
selected.cwd,
selected.git_branch
FROM (
SELECT
so.thread_id,
COALESCE(t.rollout_path, '') AS rollout_path,
so.source_updated_at,
so.raw_memory,
so.rollout_summary,
so.rollout_slug,
so.generated_at,
COALESCE(t.cwd, '') AS cwd,
t.git_branch AS git_branch
FROM stage1_outputs AS so
LEFT JOIN threads AS t
ON t.id = so.thread_id
WHERE t.memory_mode = 'enabled'
AND (length(trim(so.raw_memory)) > 0 OR length(trim(so.rollout_summary)) > 0)
AND (
(so.last_usage IS NOT NULL AND so.last_usage >= ?)
OR (so.last_usage IS NULL AND so.source_updated_at >= ?)
)
ORDER BY
COALESCE(so.usage_count, 0) DESC,
COALESCE(so.last_usage, so.source_updated_at) DESC,
so.source_updated_at DESC,
so.thread_id DESC
LIMIT ?
) AS selected
ORDER BY selected.thread_id ASC
"#,
)
.bind(cutoff)
Expand Down Expand Up @@ -1260,6 +1274,10 @@ mod tests {
use std::sync::Arc;
use uuid::Uuid;

fn stable_thread_id(value: &str) -> ThreadId {
ThreadId::from_string(value).expect("thread id")
}

#[tokio::test]
async fn stage1_claim_skips_when_up_to_date() {
let codex_home = unique_temp_dir();
Expand Down Expand Up @@ -2829,9 +2847,9 @@ VALUES (?, ?, ?, ?, ?)
.await
.expect("initialize runtime");

let thread_id_a = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id");
let thread_id_b = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id");
let thread_id_c = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id");
let thread_id_a = stable_thread_id("00000000-0000-4000-8000-000000000001");
let thread_id_b = stable_thread_id("00000000-0000-4000-8000-000000000002");
let thread_id_c = stable_thread_id("00000000-0000-4000-8000-000000000003");
let owner = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id");

for (thread_id, workspace) in [
Expand Down Expand Up @@ -2918,12 +2936,21 @@ VALUES (?, ?, ?, ?, ?)
.expect("load phase2 input selection");

assert_eq!(selection.len(), 2);
assert_eq!(selection[0].thread_id, thread_id_c);
assert_eq!(
selection[0].rollout_path,
selection
.iter()
.map(|output| output.thread_id)
.collect::<Vec<_>>(),
vec![thread_id_b, thread_id_c]
);
let selected_c = selection
.iter()
.find(|output| output.thread_id == thread_id_c)
.expect("thread c should be selected");
assert_eq!(
selected_c.rollout_path,
codex_home.join(format!("rollout-{thread_id_c}.jsonl"))
);
assert_eq!(selection[1].thread_id, thread_id_b);

let _ = tokio::fs::remove_dir_all(codex_home).await;
}
Expand Down Expand Up @@ -3235,10 +3262,10 @@ VALUES (?, ?, ?, ?, ?)
.await
.expect("initialize runtime");

let thread_id_a = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread a");
let thread_id_b = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread b");
let thread_id_c = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread c");
let thread_id_d = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread d");
let thread_id_a = stable_thread_id("00000000-0000-4000-8000-000000000001");
let thread_id_b = stable_thread_id("00000000-0000-4000-8000-000000000002");
let thread_id_c = stable_thread_id("00000000-0000-4000-8000-000000000003");
let thread_id_d = stable_thread_id("00000000-0000-4000-8000-000000000004");
let owner = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id");

for (thread_id, workspace) in [
Expand Down Expand Up @@ -3365,7 +3392,7 @@ VALUES (?, ?, ?, ?, ?)
.iter()
.map(|output| output.thread_id)
.collect::<Vec<_>>(),
vec![thread_id_d, thread_id_c]
vec![thread_id_c, thread_id_d]
);

let _ = tokio::fs::remove_dir_all(codex_home).await;
Expand Down Expand Up @@ -3768,9 +3795,9 @@ VALUES (?, ?, ?, ?, ?)

let now = Utc::now();
let owner = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id");
let thread_a = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id a");
let thread_b = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id b");
let thread_c = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id c");
let thread_a = stable_thread_id("00000000-0000-4000-8000-000000000001");
let thread_b = stable_thread_id("00000000-0000-4000-8000-000000000002");
let thread_c = stable_thread_id("00000000-0000-4000-8000-000000000003");

for (thread_id, workspace) in [
(thread_a, "workspace-a"),
Expand Down Expand Up @@ -3840,7 +3867,7 @@ VALUES (?, ?, ?, ?, ?)
}

let selection = runtime
.get_phase2_input_selection(/*n*/ 3, /*max_unused_days*/ 30)
.get_phase2_input_selection(/*n*/ 1, /*max_unused_days*/ 30)
.await
.expect("load phase2 input selection");

Expand All @@ -3849,7 +3876,7 @@ VALUES (?, ?, ?, ?, ?)
.iter()
.map(|output| output.thread_id)
.collect::<Vec<_>>(),
vec![thread_b, thread_a, thread_c]
vec![thread_b]
);

let _ = tokio::fs::remove_dir_all(codex_home).await;
Expand All @@ -3864,9 +3891,9 @@ VALUES (?, ?, ?, ?, ?)

let now = Utc::now();
let owner = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id");
let thread_a = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id a");
let thread_b = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id b");
let thread_c = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id c");
let thread_a = stable_thread_id("00000000-0000-4000-8000-000000000001");
let thread_b = stable_thread_id("00000000-0000-4000-8000-000000000002");
let thread_c = stable_thread_id("00000000-0000-4000-8000-000000000003");

for (thread_id, workspace) in [
(thread_a, "workspace-a"),
Expand Down Expand Up @@ -3945,7 +3972,7 @@ VALUES (?, ?, ?, ?, ?)
.iter()
.map(|output| output.thread_id)
.collect::<Vec<_>>(),
vec![thread_c, thread_b]
vec![thread_b, thread_c]
);

let _ = tokio::fs::remove_dir_all(codex_home).await;
Expand Down
Loading