use std::path::Path;
use crate::config::MemorySkillConfig;
use super::{
scan::{scan_memory_skill_advisory, scan_memory_skill_candidate},
store::MemorySkillStore,
types::{MemorySkillAdvisory, MemorySkillCandidate, MemorySkillError},
};
const MAX_REINJECTION_ITEMS: usize = 20;
const MAX_REINJECTION_SCAN_ITEMS: usize = MAX_REINJECTION_ITEMS * 4;
pub fn reinjection_section(
state_dir: &Path,
config: &crate::config::TruthMirrorConfig,
) -> Result<String, MemorySkillError> {
if !config.memory_skill.effective_enabled(&config.skills) {
return Ok(String::new());
}
let store = MemorySkillStore::new(state_dir, &config.memory_skill)?;
let candidates = store.pending_candidates()?;
let advisories = store
.read_advisories()?
.into_iter()
.filter(|advisory| !advisory.dismissed)
.collect::<Vec<_>>();
if candidates.is_empty() && advisories.is_empty() {
return Ok(String::new());
}
let mut items = Vec::new();
let accepted_candidates = scan_and_collect(
&candidates,
render_candidate_item,
|candidate| candidate.id.as_str(),
scan_memory_skill_candidate,
&config.memory_skill,
&mut items,
)?;
let accepted_advisories = scan_and_collect(
&advisories,
render_advisory_item,
|advisory| advisory.id.as_str(),
scan_memory_skill_advisory,
&config.memory_skill,
&mut items,
)?;
let has_omitted_backlog =
candidates.len() > accepted_candidates || advisories.len() > accepted_advisories;
if items.is_empty() && !has_omitted_backlog {
return Ok(String::new());
}
let mut output = String::new();
output.push_str("\nTruth Mirror pending memory/skill candidates:\n");
for item in items {
output.push_str(&item);
}
if candidates.len() > accepted_candidates {
output.push_str(&format!(
"- ... {} more pending candidates omitted; run `truth-mirror memory-skill list`.\n",
candidates.len() - accepted_candidates
));
}
if advisories.len() > accepted_advisories {
output.push_str(&format!(
"- ... {} more suggested candidates omitted; run `truth-mirror memory-skill list`.\n",
advisories.len() - accepted_advisories
));
}
Ok(output)
}
fn scan_and_collect<T>(
entries: &[T],
render: impl Fn(&T) -> String,
id_of: impl Fn(&T) -> &str,
scan: impl Fn(&T, &MemorySkillConfig) -> Result<(), MemorySkillError>,
config: &MemorySkillConfig,
items: &mut Vec<String>,
) -> Result<usize, MemorySkillError> {
let mut accepted = 0;
for (scanned_items, entry) in entries.iter().enumerate() {
if items.len() >= MAX_REINJECTION_ITEMS || scanned_items >= MAX_REINJECTION_SCAN_ITEMS {
break;
}
if scan_reinjection_item(entry, config, id_of(entry), &scan)? {
let item = render(entry);
items.push(item);
accepted += 1;
}
}
Ok(accepted)
}
fn render_candidate_item(candidate: &MemorySkillCandidate) -> String {
format!(
"- {} {} {}\n source: {} {}\n occurrences: {}\n action: human review required outside the agent runtime\n inspect: `truth-mirror memory-skill show {}`\n",
candidate.id,
candidate.candidate_kind.as_str(),
candidate.slug,
candidate.source_commit,
candidate.truth_label,
candidate.occurrence_count,
candidate.id
)
}
fn render_advisory_item(advisory: &MemorySkillAdvisory) -> String {
format!(
"- {} {} {} (suggested)\n source: {} {}\n occurrences: {}\n action: human review required outside the agent runtime\n inspect: `truth-mirror memory-skill list`\n",
advisory.id,
advisory.candidate_kind.as_str(),
advisory.slug,
advisory.source_commit,
advisory.truth_label,
advisory.occurrence_count
)
}
fn scan_reinjection_item<T>(
entry: &T,
config: &MemorySkillConfig,
id: &str,
scan: &impl Fn(&T, &MemorySkillConfig) -> Result<(), MemorySkillError>,
) -> Result<bool, MemorySkillError> {
match scan(entry, config) {
Ok(()) => Ok(true),
Err(MemorySkillError::ScanRejected { reason }) => {
tracing::warn!(
item_id = id,
reason = %reason,
"skipping unsafe memory-skill reinjection item"
);
Ok(false)
}
Err(error) => Err(error),
}
}