fix(memory): stamp memory freshness so stale context is never served as current (#2944) (#2979)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
This commit is contained in:
sanil-23
2026-05-29 14:04:44 -07:00
committed by GitHub
co-authored by Claude Opus 4.8 Steven Enamakel
parent 2920e4724a
commit 1ecc42d6b4
9 changed files with 375 additions and 31 deletions
@@ -33,4 +33,5 @@ Prepare a morning briefing that helps the user start their day with clarity. Pul
- **Never fabricate events, emails, or tasks.** Only include data you actually retrieved from tools or memory.
- **Respect time zones.** The system prompt below carries the user's local date/time and IANA timezone — read it from there. Do **not** ask the user to repeat their timezone; only fall back to UTC and note it if the system context is genuinely missing the field.
- **No stale data.** If a tool call fails or returns empty, say so — don't fall back to yesterday's data.
- **Honor the timeline of memory.** Memory and prior-conversation context are stamped with when they were last updated (e.g. `(last updated 2026-05-25)`, `(as of …)`, `(noted …)`). Compare every such date against today's date in the system context. If an item predates the day you're briefing for, treat it as background — never restate an older daily summary, reminder, or "today you have…" note as if it is current. When the only relevant data is stale, name its date explicitly ("from your May 25 notes…") instead of presenting it as today's.
- **Privacy first.** Don't include full email bodies or message contents. Summarize senders and subjects.
+11 -2
View File
@@ -33,7 +33,9 @@ use crate::openhuman::agent_experience::{
prepend_experience_block, render_experience_hits, AgentExperienceStore, ExperienceQuery,
};
use crate::openhuman::agent_tool_policy::render_tool_policy_boundary;
use crate::openhuman::context::prompt::{LearnedContextData, PromptContext, PromptTool};
use crate::openhuman::context::prompt::{
LearnedContextData, NamespaceSummary, PromptContext, PromptTool,
};
use crate::openhuman::context::{ReductionOutcome, ARCHIVIST_EXTRACTION_PROMPT};
use crate::openhuman::inference::model_context::context_window_for_model;
use crate::openhuman::inference::provider::{
@@ -2571,12 +2573,19 @@ fn collect_tree_root_summaries(
workspace_dir: &std::path::Path,
per_namespace_cap: usize,
total_cap: usize,
) -> Vec<(String, String)> {
) -> Vec<NamespaceSummary> {
crate::openhuman::memory_tree::tree_runtime::store::collect_root_summaries_with_caps(
workspace_dir,
per_namespace_cap,
total_cap,
)
.into_iter()
.map(|(namespace, body, updated_at)| NamespaceSummary {
namespace,
body,
updated_at,
})
.collect()
}
/// Sanitize a learned memory entry before injecting into the system prompt.
@@ -446,6 +446,49 @@ fn build_parent_context_and_sanitize_helpers_cover_snapshot_paths() {
assert!(collect_tree_root_summaries(agent.workspace_dir(), 8_000, 32_000).is_empty());
}
#[test]
fn collect_tree_root_summaries_maps_namespace_body_and_timestamp() {
// #2944: the wrapper must carry the root node's `updated_at` from the
// store tuple into the `NamespaceSummary` the prompt renderer stamps.
use crate::openhuman::config::Config;
use crate::openhuman::memory_tree::tree_runtime::store::write_node;
use crate::openhuman::memory_tree::tree_runtime::types::{
derive_parent_id, estimate_tokens, level_from_node_id, TreeNode,
};
let tmp = tempfile::TempDir::new().unwrap();
let workspace = tmp.path().join("workspace");
std::fs::create_dir_all(&workspace).unwrap();
let config = Config {
workspace_dir: workspace.clone(),
..Config::default()
};
let updated_at = chrono::DateTime::parse_from_rfc3339("2026-05-25T09:00:00Z")
.unwrap()
.with_timezone(&chrono::Utc);
let summary = "Distilled activities summary.";
let node = TreeNode {
node_id: "root".to_string(),
namespace: "activities".to_string(),
level: level_from_node_id("root"),
parent_id: derive_parent_id("root"),
summary: summary.to_string(),
token_count: estimate_tokens(summary),
child_count: 0,
created_at: updated_at,
updated_at,
metadata: None,
};
write_node(&config, &node).unwrap();
let summaries = collect_tree_root_summaries(&workspace, 8_000, 32_000);
assert_eq!(summaries.len(), 1);
assert_eq!(summaries[0].namespace, "activities");
assert_eq!(summaries[0].body, summary);
assert_eq!(summaries[0].updated_at, updated_at);
}
#[tokio::test]
async fn transcript_roundtrip_work() {
let mut agent = make_agent(None);
+105 -2
View File
@@ -21,6 +21,27 @@ const PRIOR_CONVERSATION_LIMIT: usize = 3;
/// do not auto-pollute every fresh chat.
const PRIOR_CONVERSATION_KEY_PREFIX: &str = "high.";
/// Parse a `MemoryEntry::timestamp` (RFC 3339) into an absolute
/// `YYYY-MM-DD` label for prompt injection, e.g. `2026-05-25`. Returns
/// `None` when the timestamp is missing or unparseable so callers omit
/// the stamp rather than emit a garbage date.
///
/// Time-sensitive memory ("finish the proposal by Wednesday") is a prime
/// vector for stale-as-current hallucinations: with no date the model
/// can't tell a four-day-old working fact from a present-tense one, so it
/// may serve it as today's — the same failure as the memory-tree path.
/// This block feeds the chat user message *and*, via
/// `last_memory_context`, every typed sub-agent including the cron
/// morning briefing (#2944). Reuses the prompt layer's absolute-date
/// formatter for one consistent date shape across surfaces.
fn memory_entry_date_label(timestamp: &str) -> Option<String> {
chrono::DateTime::parse_from_rfc3339(timestamp)
.ok()
.map(|dt| {
crate::openhuman::agent::prompts::memory_date_label(dt.with_timezone(&chrono::Utc))
})
}
/// Canonical header for the `[Cross-chat context]` block injected on
/// every turn that has FTS-surfaced hits from other threads.
///
@@ -202,7 +223,13 @@ impl MemoryLoader for DefaultMemoryLoader {
context.push_str(section);
appended_working_header = true;
}
let line = format!("- {}: {}\n", entry.key, entry.content);
// Stamp each fact with its last-updated date so the model can
// compare against the current date and not present a stale
// working fact as current (#2944).
let line = match memory_entry_date_label(&entry.timestamp) {
Some(date) => format!("- {} (as of {date}): {}\n", entry.key, entry.content),
None => format!("- {}: {}\n", entry.key, entry.content),
};
if context.len() + line.len() > budget {
tracing::debug!(
budget,
@@ -271,7 +298,12 @@ impl MemoryLoader for DefaultMemoryLoader {
context.push_str(section);
appended_prior_header = true;
}
let line = format!("- {primary}\n");
// Date-stamp the fact so a months-old "high importance"
// statement isn't read as a present-tense commitment (#2944).
let line = match memory_entry_date_label(&entry.timestamp) {
Some(date) => format!("- (noted {date}) {primary}\n"),
None => format!("- {primary}\n"),
};
if context.len() + line.len() > budget {
tracing::debug!(
budget,
@@ -514,6 +546,77 @@ mod tests {
}
}
#[test]
fn memory_entry_date_label_parses_rfc3339_else_none() {
assert_eq!(
super::memory_entry_date_label("2026-05-25T07:00:00Z").as_deref(),
Some("2026-05-25")
);
assert_eq!(super::memory_entry_date_label("not-a-date"), None);
assert_eq!(super::memory_entry_date_label(""), None);
}
#[tokio::test]
async fn loader_stamps_working_memory_with_date() {
// #2944: working-memory facts must carry their last-updated date so
// the model (and downstream sub-agents / the cron briefing, which
// inherit this block) can tell a stale fact from a current one.
let mem = MockMemory::new(vec![MemoryEntry {
id: "id-tz".into(),
key: "working.user.commitment".into(),
content: "Finish the proposal by Wednesday.".into(),
namespace: Some("test".into()),
category: MemoryCategory::Conversation,
timestamp: "2026-05-25T00:00:00Z".into(),
session_id: None,
score: None,
}]);
let out = DefaultMemoryLoader::default()
.load_context(&mem, "what's on my plate?")
.await
.expect("loader must succeed");
assert!(
out.contains("[User working memory]"),
"expected working-memory block, got:\n{out}"
);
assert!(
out.contains("(as of 2026-05-25)"),
"working-memory fact must carry its date (#2944), got:\n{out}"
);
}
#[tokio::test]
async fn loader_stamps_prior_conversation_with_date() {
// #2944: high-importance prior-chat facts must be dated so a
// months-old statement isn't read as a present-tense commitment.
let mem = MockMemory::new(vec![MemoryEntry {
id: "id-1".into(),
key: "high.preference.aaaaaaaaaaaa".into(),
content: "[high preference] I prefer Postgres for new services.".into(),
namespace: Some(super::CONVERSATION_MEMORY_NAMESPACE.to_string()),
category: MemoryCategory::Conversation,
timestamp: "2026-04-22T00:00:00Z".into(),
session_id: Some("thr_old".into()),
score: Some(0.9),
}]);
let out = DefaultMemoryLoader::default()
.load_context(&mem, "what should I default to for storage?")
.await
.expect("loader must succeed");
assert!(
out.contains("[Prior conversations]"),
"expected prior conversations block, got:\n{out}"
);
assert!(
out.contains("(noted 2026-04-22)"),
"prior-conversation fact must carry its date (#2944), got:\n{out}"
);
}
#[tokio::test]
async fn loader_surfaces_prior_conversation_high_importance_only() {
// Prior chat extracted two memories: one high-importance preference
+37 -5
View File
@@ -6,7 +6,7 @@ pub use connected_identities::render_connected_identities;
use crate::openhuman::skills::Skill;
use crate::openhuman::tools::Tool;
use anyhow::Result;
use chrono::Local;
use chrono::{DateTime, Local, Utc};
use std::fmt::Write;
use std::hash::{Hash, Hasher};
use std::path::Path;
@@ -734,6 +734,20 @@ impl PromptSection for UserReflectionsSection {
}
}
/// Format a memory item's `updated_at` as an absolute UTC date label
/// for prompt injection, e.g. `2026-05-25`.
///
/// Absolute (not relative "N days ago") on purpose: memory sections sit
/// near the front of the KV-cache-stable system prompt, so a label that
/// changes daily would bust the cached prefix for everything after it.
/// An absolute date only changes when the underlying memory does. The
/// model judges staleness by comparing this against the injected current
/// date. Shared by [`UserMemorySection`] and the working-memory block in
/// `agent::memory_loader`. (#2944)
pub(crate) fn memory_date_label(updated_at: DateTime<Utc>) -> String {
updated_at.format("%Y-%m-%d").to_string()
}
impl PromptSection for UserMemorySection {
fn name(&self) -> &str {
"user_memory"
@@ -749,16 +763,34 @@ impl PromptSection for UserMemorySection {
"Long-term memory distilled by the tree summarizer. \
Each section is the root summary for a memory namespace, \
representing everything we've learned about that domain over time. \
Treat this as durable context: the model has seen these facts before, \
they should not need to be re-discovered.\n\n",
Treat this as durable background context, but NOT as fresh, \
present-tense fact: each section header shows when that memory \
was last updated. Compare those dates against the `## Current \
Date & Time` section below before answering time-sensitive \
questions (today's briefing, daily summary, reminders, calendar, \
notifications, \"today/tomorrow/this week\"). If a summary predates \
the period the user is asking about, treat it as potentially \
stale — say so explicitly and never present older memory as \
today's update.\n\n",
);
for (namespace, body) in &ctx.learned.tree_root_summaries {
for NamespaceSummary {
namespace,
body,
updated_at,
} in &ctx.learned.tree_root_summaries
{
let trimmed = body.trim();
if trimmed.is_empty() {
continue;
}
let _ = writeln!(out, "### {namespace}\n");
// Absolute date (not "N days ago") keeps this front-of-prompt
// section byte-stable for KV-cache reuse — see `NamespaceSummary`.
let _ = writeln!(
out,
"### {namespace} (last updated {})\n",
memory_date_label(*updated_at)
);
out.push_str(trimmed);
out.push_str("\n\n");
}
+77 -11
View File
@@ -6,6 +6,24 @@ use std::sync::LazyLock;
static NO_FILTER: LazyLock<HashSet<String>> = LazyLock::new(HashSet::new);
/// Build a `NamespaceSummary` with a fixed `updated_at` (#2944), so
/// freshness-label assertions are deterministic.
fn ns_summary_at(namespace: &str, body: &str, rfc3339: &str) -> NamespaceSummary {
NamespaceSummary {
namespace: namespace.into(),
body: body.into(),
updated_at: chrono::DateTime::parse_from_rfc3339(rfc3339)
.unwrap()
.with_timezone(&chrono::Utc),
}
}
/// `NamespaceSummary` with an arbitrary fixed date, for tests that don't
/// assert on the freshness stamp itself.
fn ns_summary(namespace: &str, body: &str) -> NamespaceSummary {
ns_summary_at(namespace, body, "2026-01-01T00:00:00Z")
}
struct TestTool;
#[async_trait]
@@ -377,10 +395,15 @@ fn tools_section_uses_pformat_signature_for_text_dispatchers() {
fn user_memory_section_renders_namespaces_with_headings() {
let learned = LearnedContextData {
tree_root_summaries: vec![
("user".into(), "Steven prefers terse Rust answers.".into()),
(
"conversations".into(),
"Recent thread: prompt rework.".into(),
ns_summary_at(
"user",
"Steven prefers terse Rust answers.",
"2026-05-25T00:00:00Z",
),
ns_summary_at(
"conversations",
"Recent thread: prompt rework.",
"2026-05-25T00:00:00Z",
),
],
..Default::default()
@@ -408,8 +431,54 @@ fn user_memory_section_renders_namespaces_with_headings() {
};
let rendered = UserMemorySection.build(&ctx).unwrap();
assert!(rendered.starts_with("## User Memory\n\n"));
assert!(rendered.contains("### user\n\nSteven prefers terse Rust answers."));
assert!(rendered.contains("### conversations\n\nRecent thread: prompt rework."));
assert!(
rendered
.contains("### user (last updated 2026-05-25)\n\nSteven prefers terse Rust answers."),
"heading must carry the absolute update date (#2944); got:\n{rendered}"
);
assert!(rendered
.contains("### conversations (last updated 2026-05-25)\n\nRecent thread: prompt rework."));
}
#[test]
fn memory_date_label_formats_absolute_utc_date() {
let dt = chrono::DateTime::parse_from_rfc3339("2026-05-25T18:30:00Z")
.unwrap()
.with_timezone(&chrono::Utc);
// Absolute date, no time-of-day — must stay byte-stable day to day.
assert_eq!(memory_date_label(dt), "2026-05-25");
}
#[test]
fn user_memory_section_labels_stale_summary_and_warns_against_present_tense() {
// #2944 regression: a summary last updated weeks ago must render with
// its absolute date, and the section must steer the model to compare
// against the current date — so a May-25 briefing is never served as
// today's.
let learned = LearnedContextData {
tree_root_summaries: vec![ns_summary_at(
"briefings",
"Daily briefing: 2 meetings, proposal due.",
"2026-05-25T07:00:00Z",
)],
..Default::default()
};
let rendered = UserMemorySection.build(&ctx_with_learned(learned)).unwrap();
assert!(
rendered.contains("### briefings (last updated 2026-05-25)"),
"stale summary must carry its absolute update date; got:\n{rendered}"
);
// Guardrail: tell the model to cross-check against the current date
// and not restate older memory as today's.
assert!(
rendered.contains("Current Date & Time"),
"section must reference the current-date block; got:\n{rendered}"
);
assert!(
rendered.contains("never present older memory as"),
"section must forbid presenting stale memory as current; got:\n{rendered}"
);
}
#[test]
@@ -1198,10 +1267,7 @@ fn prompt_tool_constructors_and_user_memory_skip_empty_bodies() {
skills: &[],
dispatcher_instructions: "",
learned: LearnedContextData {
tree_root_summaries: vec![
("user".into(), "kept".into()),
("empty".into(), " ".into()),
],
tree_root_summaries: vec![ns_summary("user", "kept"), ns_summary("empty", " ")],
..Default::default()
},
visible_tool_names: &NO_FILTER,
@@ -1338,7 +1404,7 @@ fn user_reflections_render_above_user_memory_when_both_present() {
// UserMemorySection content).
let ctx = ctx_with_learned(LearnedContextData {
reflections: vec!["I want terse answers".into()],
tree_root_summaries: vec![("user".into(), "Generic summary".into())],
tree_root_summaries: vec![ns_summary("user", "Generic summary")],
..Default::default()
});
let reflections = UserReflectionsSection.build(&ctx).unwrap();
+29 -3
View File
@@ -9,6 +9,7 @@
use crate::openhuman::skills::Skill;
use crate::openhuman::tools::Tool;
use anyhow::Result;
use chrono::{DateTime, Utc};
use std::path::Path;
// ─────────────────────────────────────────────────────────────────────────────
@@ -68,9 +69,34 @@ pub struct LearnedContextData {
/// subsystem is off or no reflections have been captured yet.
pub reflections: Vec<String>,
/// Pre-fetched root-level summaries from the tree summarizer, one per
/// namespace that has a root node on disk. Each entry is
/// `(namespace, body)`. Empty when the tree summarizer hasn't run.
pub tree_root_summaries: Vec<(String, String)>,
/// namespace that has a root node on disk. Empty when the tree
/// summarizer hasn't run.
///
/// Each entry carries the namespace's root `updated_at` so the
/// renderer can stamp how current the memory is. Without that stamp
/// the model treats distilled memory as present-tense and can serve
/// a stale summary as today's update (#2944).
pub tree_root_summaries: Vec<NamespaceSummary>,
}
/// A single memory-namespace root summary fetched from the tree
/// summarizer, paired with the timestamp of its root node.
///
/// `updated_at` is rendered as an absolute date (not a relative
/// "N days ago") on purpose: this block sits near the front of the
/// KV-cache-stable system prompt, so a label that changes every day
/// would bust the cached prefix for everything after it. An absolute
/// date only changes when the underlying memory does; the model judges
/// freshness by comparing it against the `## Current Date & Time`
/// section. See [`LearnedContextData::tree_root_summaries`] (#2944).
#[derive(Debug, Clone)]
pub struct NamespaceSummary {
/// Memory namespace this root summary belongs to (e.g. `activities`).
pub namespace: String,
/// The distilled root summary text.
pub body: String,
/// When the namespace's root node was last updated on disk.
pub updated_at: DateTime<Utc>,
}
// ─────────────────────────────────────────────────────────────────────────────
@@ -341,14 +341,19 @@ pub fn get_tree_status(config: &Config, namespace: &str) -> Result<TreeStatus> {
/// and silently dropped — user memory is best-effort context, never a
/// hard requirement for running a turn or rendering a prompt dump.
///
/// Returns a stable-ordered `Vec<(namespace, body)>` so byte-identical
/// inputs produce byte-identical output across process restarts (the
/// renderer downstream relies on this for KV-cache prefix reuse).
/// Returns a stable-ordered `Vec<(namespace, body, updated_at)>` so
/// byte-identical inputs produce byte-identical output across process
/// restarts (the renderer downstream relies on this for KV-cache prefix
/// reuse). The `updated_at` is the namespace root node's timestamp, so
/// the prompt renderer can stamp how current each summary is and the
/// model never serves stale memory as today's (#2944). A plain tuple
/// (rather than a prompt-layer struct) keeps this module free of any
/// `agent::prompts` dependency.
pub fn collect_root_summaries_with_caps(
workspace_dir: &Path,
per_namespace_cap: usize,
total_cap: usize,
) -> Vec<(String, String)> {
) -> Vec<(String, String, DateTime<Utc>)> {
// The store functions all read `config.workspace_dir` and nothing
// else, so we shim a tiny `Config` from the caller's path. Cheap
// (a few allocations) and avoids forcing every call site to thread
@@ -440,7 +445,7 @@ pub fn collect_root_summaries_with_caps(
running_total = total_chars,
"[tree_summarizer] including namespace in root-summary collection"
);
out.push((ns, final_body));
out.push((ns, final_body, node.updated_at));
}
tracing::info!(
@@ -760,17 +765,27 @@ fn parse_node_markdown(raw: &str, namespace: &str, node_id: &str) -> Result<Tree
.and_then(|v| v.parse::<u32>().ok())
.unwrap_or(0);
// Deterministic fallbacks. `updated_at` now flows into the system
// prompt as a freshness stamp (#2944), and the renderer relies on
// byte-identical output across process restarts for KV-cache reuse —
// `Utc::now()` would change on every read and break that, *and* would
// misrepresent an undated legacy node as "fresh today" (the exact
// stale-as-current bug this work fixes). For nodes that predate the
// `created_at`/`updated_at` frontmatter we fall back to UNIX_EPOCH so
// the date is stable and conservatively renders as ancient rather
// than current. `updated_at` falls back to `created_at` (its best
// available estimate of when the node was last touched).
let created_at = frontmatter
.get("created_at")
.and_then(|v| DateTime::parse_from_rfc3339(v).ok())
.map(|dt| dt.with_timezone(&Utc))
.unwrap_or_else(Utc::now);
.unwrap_or(DateTime::<Utc>::UNIX_EPOCH);
let updated_at = frontmatter
.get("updated_at")
.and_then(|v| DateTime::parse_from_rfc3339(v).ok())
.map(|dt| dt.with_timezone(&Utc))
.unwrap_or_else(Utc::now);
.unwrap_or(created_at);
let metadata = frontmatter.get("metadata").map(|v| v.to_string());
@@ -204,6 +204,34 @@ fn frontmatter_parsing() {
assert_eq!(body, "Hello world.");
}
#[test]
fn parse_node_markdown_uses_deterministic_fallback_timestamps() {
// #2944: `updated_at` now flows into the system prompt as a freshness
// stamp, and the renderer relies on byte-stable output for KV-cache
// reuse. Legacy nodes that predate the `created_at`/`updated_at`
// frontmatter must therefore fall back deterministically — never to
// `Utc::now()`, which would change on every read and masquerade an
// undated node as "fresh today".
// No timestamps at all → both fall back to UNIX_EPOCH (renders as an
// unmistakably ancient date rather than today's).
let raw = "---\nnode_id: \"root\"\nlevel: root\n---\n\nUndated summary.";
let node = parse_node_markdown_pub(raw, "ns", "root").unwrap();
assert_eq!(node.created_at, DateTime::<Utc>::UNIX_EPOCH);
assert_eq!(node.updated_at, DateTime::<Utc>::UNIX_EPOCH);
// `created_at` present but `updated_at` missing → `updated_at` falls
// back to `created_at`, its best estimate of last-touched.
let raw =
"---\nnode_id: \"root\"\nlevel: root\ncreated_at: 2026-05-25T09:00:00Z\n---\n\nSummary.";
let node = parse_node_markdown_pub(raw, "ns", "root").unwrap();
let created = DateTime::parse_from_rfc3339("2026-05-25T09:00:00Z")
.unwrap()
.with_timezone(&Utc);
assert_eq!(node.created_at, created);
assert_eq!(node.updated_at, created);
}
#[test]
fn validate_node_id_accepts_valid() {
assert!(validate_node_id("root").is_ok());
@@ -278,7 +306,7 @@ fn collect_root_summaries_respects_per_namespace_cap() {
// Per-namespace cap of 10 should clip the body.
let result = collect_root_summaries_with_caps(&config.workspace_dir, 10, 10_000);
assert_eq!(result.len(), 1);
let (ns, body) = &result[0];
let (ns, body, _updated_at) = &result[0];
assert_eq!(ns, "ns");
assert!(
body.starts_with("xxxxxxxxxx"),
@@ -287,6 +315,27 @@ fn collect_root_summaries_respects_per_namespace_cap() {
assert!(body.contains("[... truncated]"));
}
#[test]
fn collect_root_summaries_carries_root_updated_at() {
// #2944: the namespace root's `updated_at` must survive the
// disk round-trip so the prompt renderer can stamp memory freshness.
let tmp = TempDir::new().unwrap();
let config = test_config(&tmp);
let fixed = chrono::DateTime::parse_from_rfc3339("2026-05-25T09:00:00Z")
.unwrap()
.with_timezone(&Utc);
let mut node = make_node("ns", "root", "summary body");
node.updated_at = fixed;
write_node(&config, &node).unwrap();
let result = collect_root_summaries_with_caps(&config.workspace_dir, 1000, 10_000);
assert_eq!(result.len(), 1);
let (ns, _body, updated_at) = &result[0];
assert_eq!(ns, "ns");
assert_eq!(*updated_at, fixed, "root updated_at must round-trip");
}
#[test]
fn collect_root_summaries_stops_at_total_cap() {
let tmp = TempDir::new().unwrap();