Skip to content

rebuild_system_prompt loads skill outcome stats from SQLite twice per turn #6266

Description

@bug-ops

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.

Metadata

Metadata

Assignees

Labels

P2High value, medium complexityarchitectureArchitecture improvementsmemoryzeph-memory crate (SQLite)performancePerformance improvementsskillszeph-skills crate

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions