Finding
Agent::rebuild_system_prompt (crates/zeph-core/src/agent/context/assembly.rs) issues two separate memory.sqlite().load_skill_outcome_stats() calls per turn — once inside match_and_rank_skills (building metrics_map: HashMap<String, (u32, u32)> for trust/RL reranking) and once directly in rebuild_system_prompt (building health_map: HashMap<String, (f64, u32)> for the skills-prompt XML attributes). Both derive from the exact same underlying query (successes/failures per skill name) with no intervening mutation of skill outcome stats between the two calls within a single turn.
This runs on the hot per-turn system-prompt-rebuild path (CLAUDE.md non-blocking hot-path contract: "system prompt rebuild, memory injection, summarization, compaction"), so it costs an extra unconditional SQLite round-trip every turn for data already fetched moments earlier.
Location
crates/zeph-core/src/agent/context/assembly.rs:782 — first call, inside match_and_rank_skills (only when !scored.is_empty()), builds metrics_map: HashMap<String, (u32, u32)> (successes, failures) used for trust_score::rerank and RL-head candidate stats.
crates/zeph-core/src/agent/context/assembly.rs:1148 — second call, unconditional in rebuild_system_prompt, builds health_map: HashMap<String, (f64, u32)> (posterior_mean, total) for format_active_skills_prompt's XML attributes.
Before
// match_and_rank_skills (~line 1144-1162)
let metrics_map: std::collections::HashMap<String, (u32, u32)> =
if let Some(memory) = &self.services.memory.persistence.memory {
memory.sqlite().load_skill_outcome_stats().await.unwrap_or_default()
.into_iter()
.map(|m| { /* -> (successes, failures) */ })
.collect()
} else { HashMap::new() };
// rebuild_system_prompt (~line 776-796), called right after match_and_rank_skills returns
let health_map: std::collections::HashMap<String, (f64, u32)> = if let Some(memory) =
&self.services.memory.persistence.memory
{
memory.sqlite().load_skill_outcome_stats().await.unwrap_or_default()
.into_iter()
.map(|m| { /* -> (posterior_mean, total) */ })
.collect()
} else { HashMap::new() };
After
Fetch the raw stats once in rebuild_system_prompt, pass the shared Vec/map down into match_and_rank_skills (or hoist both derived maps from a single fetch), so load_skill_outcome_stats() executes at most once per turn.
Why
Same query, same data, two round trips — a straightforward redundant-I/O DRY violation on a documented hot path. Low risk to fix (pure data reuse, no behavior change) since both derived maps are pure functions of the same source rows.
Finding
Agent::rebuild_system_prompt(crates/zeph-core/src/agent/context/assembly.rs) issues two separatememory.sqlite().load_skill_outcome_stats()calls per turn — once insidematch_and_rank_skills(buildingmetrics_map: HashMap<String, (u32, u32)>for trust/RL reranking) and once directly inrebuild_system_prompt(buildinghealth_map: HashMap<String, (f64, u32)>for the skills-prompt XML attributes). Both derive from the exact same underlying query (successes/failuresper skill name) with no intervening mutation of skill outcome stats between the two calls within a single turn.This runs on the hot per-turn system-prompt-rebuild path (CLAUDE.md non-blocking hot-path contract: "system prompt rebuild, memory injection, summarization, compaction"), so it costs an extra unconditional SQLite round-trip every turn for data already fetched moments earlier.
Location
crates/zeph-core/src/agent/context/assembly.rs:782— first call, insidematch_and_rank_skills(only when!scored.is_empty()), buildsmetrics_map: HashMap<String, (u32, u32)>(successes, failures) used fortrust_score::rerankand RL-head candidate stats.crates/zeph-core/src/agent/context/assembly.rs:1148— second call, unconditional inrebuild_system_prompt, buildshealth_map: HashMap<String, (f64, u32)>(posterior_mean, total) forformat_active_skills_prompt's XML attributes.Before
After
Fetch the raw stats once in
rebuild_system_prompt, pass the sharedVec/map down intomatch_and_rank_skills(or hoist both derived maps from a single fetch), soload_skill_outcome_stats()executes at most once per turn.Why
Same query, same data, two round trips — a straightforward redundant-I/O DRY violation on a documented hot path. Low risk to fix (pure data reuse, no behavior change) since both derived maps are pure functions of the same source rows.