use super::{MigrateError, MigrationResult, insert_after_section};
pub fn migrate_forgetting_config(toml_src: &str) -> Result<MigrationResult, MigrateError> {
if toml_src.contains("[memory.forgetting]") || toml_src.contains("# [memory.forgetting]") {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let doc = toml_src.parse::<toml_edit::DocumentMut>()?;
if !doc.contains_key("memory") {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let comment = "\n# SleepGate forgetting sweep (#2397). Disabled by default.\n\
# [memory.forgetting]\n\
# enabled = false\n\
# decay_rate = 0.1 # per-sweep importance decay\n\
# forgetting_floor = 0.05 # prune below this score\n\
# sweep_interval_secs = 7200 # run every 2 hours\n\
# sweep_batch_size = 500\n\
# protect_recent_hours = 24\n\
# protect_min_access_count = 3\n";
let raw = doc.to_string();
let output = format!("{raw}{comment}");
Ok(MigrationResult {
output,
changed_count: 1,
sections_changed: vec!["memory.forgetting".to_owned()],
})
}
pub fn migrate_memory_graph_config(toml_src: &str) -> Result<MigrationResult, MigrateError> {
if toml_src.contains("retrieval_strategy")
|| toml_src.contains("[memory.graph.beam_search]")
|| toml_src.contains("# [memory.graph.beam_search]")
{
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let comment = "\n# [memory.graph] retrieval strategy options (#3311).\n\
# retrieval_strategy = \"synapse\" # synapse | bfs | astar | watercircles | beam_search | hybrid\n\
#\n\
# [memory.graph.beam_search] # active when retrieval_strategy = \"beam_search\"\n\
# beam_width = 10 # top-K candidates kept per hop\n\
#\n\
# [memory.graph.watercircles] # active when retrieval_strategy = \"watercircles\"\n\
# ring_limit = 0 # max facts per ring; 0 = auto\n\
#\n\
# [memory.graph.experience] # experience memory recording\n\
# enabled = false\n\
# evolution_sweep_enabled = false\n\
# confidence_prune_threshold = 0.1 # prune edges below this threshold\n\
# evolution_sweep_interval = 50 # turns between sweeps\n";
let output = format!("{toml_src}{comment}");
Ok(MigrationResult {
output,
changed_count: 1,
sections_changed: vec!["memory.graph.retrieval".to_owned()],
})
}
pub fn migrate_memory_retrieval_config(toml_src: &str) -> Result<MigrationResult, MigrateError> {
if toml_src
.lines()
.any(|l| l.trim() == "[memory.retrieval]" || l.trim() == "# [memory.retrieval]")
{
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let comment = "\n# [memory.retrieval] — MemMachine-inspired retrieval tuning (#3340, #3341).\n\
# [memory.retrieval]\n\
# depth = 0 # ANN candidates fetched from the vector store, directly.\n\
# # 0 = legacy behavior (recall_limit * 2). Set to an explicit\n\
# # value >= recall_limit * 2 to enlarge the candidate pool.\n\
# search_prompt_template = \"\" # embedding query template; {query} = raw user query; empty = identity\n\
# context_format = \"structured\" # structured | plain — memory snippet rendering format\n\
# query_bias_correction = true # shift first-person queries towards user profile centroid (MM-F3)\n\
# query_bias_profile_weight = 0.25 # blend weight [0.0, 1.0]; 0.0 = off, 1.0 = full centroid\n\
# query_bias_centroid_ttl_secs = 300 # seconds before profile centroid cache is recomputed\n";
let output = format!("{toml_src}{comment}");
Ok(MigrationResult {
output,
changed_count: 1,
sections_changed: vec!["memory.retrieval".to_owned()],
})
}
pub fn migrate_memory_reasoning_config(toml_src: &str) -> Result<MigrationResult, MigrateError> {
if toml_src
.lines()
.any(|l| l.trim() == "[memory.reasoning]" || l.trim() == "# [memory.reasoning]")
{
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let comment = "\n# [memory.reasoning] — ReasoningBank: distilled strategy memory (#3369).\n\
# [memory.reasoning]\n\
# enabled = false\n\
# extract_provider = \"\" # SLM: self-judge (JSON response) — leave blank to use primary\n\
# distill_provider = \"\" # SLM: strategy distillation — leave blank to use primary\n\
# top_k = 3 # strategies injected per turn\n\
# store_limit = 1000 # max rows in reasoning_strategies table\n\
# context_budget_tokens = 500\n\
# extraction_timeout_secs = 30\n\
# distill_timeout_secs = 30\n\
# max_messages = 6\n\
# min_messages = 2\n\
# max_message_chars = 2000\n";
let output = format!("{toml_src}{comment}");
Ok(MigrationResult {
output,
changed_count: 1,
sections_changed: vec!["memory.reasoning".to_owned()],
})
}
pub fn migrate_memory_reasoning_judge_config(
toml_src: &str,
) -> Result<MigrationResult, MigrateError> {
let has_section = toml_src.lines().any(|l| l.trim() == "[memory.reasoning]");
if !has_section {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let has_window = toml_src.lines().any(|l| {
let t = l.trim().trim_start_matches('#').trim();
t.starts_with("self_judge_window")
});
let has_min_chars = toml_src.lines().any(|l| {
let t = l.trim().trim_start_matches('#').trim();
t.starts_with("min_assistant_chars")
});
if has_window && has_min_chars {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let lines: Vec<&str> = toml_src.lines().collect();
let mut section_start = None;
let mut insert_after = None;
for (i, line) in lines.iter().enumerate() {
if line.trim() == "[memory.reasoning]" {
section_start = Some(i);
}
if let Some(start) = section_start {
let trimmed = line.trim();
if i > start && trimmed.starts_with('[') && !trimmed.starts_with("[[") {
break;
}
insert_after = Some(i);
}
}
let Some(insert_idx) = insert_after else {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
};
let mut new_lines: Vec<String> = lines.iter().map(|l| (*l).to_owned()).collect();
let mut additions = Vec::new();
if !has_window {
additions.push(
"# self_judge_window = 2 # max recent messages passed to self-judge (#3383)"
.to_owned(),
);
}
if !has_min_chars {
additions.push(
"# min_assistant_chars = 50 # skip self-judge for short replies (#3383)".to_owned(),
);
}
for (offset, line) in additions.iter().enumerate() {
new_lines.insert(insert_idx + 1 + offset, line.clone());
}
let output = new_lines.join("\n") + if toml_src.ends_with('\n') { "\n" } else { "" };
Ok(MigrationResult {
output,
changed_count: additions.len(),
sections_changed: vec!["memory.reasoning".to_owned()],
})
}
pub fn migrate_memory_hebbian_config(toml_src: &str) -> Result<MigrationResult, MigrateError> {
if toml_src
.lines()
.any(|l| l.trim() == "[memory.hebbian]" || l.trim() == "# [memory.hebbian]")
{
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let comment = "\n# [memory.hebbian] # HL-F1/F2 (#3344) Hebbian edge reinforcement\n\
# [memory.hebbian]\n\
# enabled = false # opt-in master switch; no DB writes when false\n\
# hebbian_lr = 0.1 # weight increment per co-activation (0.01–0.5)\n";
let output = format!("{toml_src}{comment}");
Ok(MigrationResult {
output,
changed_count: 1,
sections_changed: vec!["memory.hebbian".to_owned()],
})
}
pub fn migrate_memory_hebbian_consolidation_config(
toml_src: &str,
) -> Result<MigrationResult, MigrateError> {
let has_section = toml_src.lines().any(|l| l.trim() == "[memory.hebbian]");
if !has_section {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let has_interval = toml_src
.lines()
.any(|l| l.trim().starts_with("consolidation_interval_secs"));
let has_threshold = toml_src
.lines()
.any(|l| l.trim().starts_with("consolidation_threshold"));
let has_provider = toml_src
.lines()
.any(|l| l.trim().starts_with("consolidate_provider"));
if has_interval && has_threshold && has_provider {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let extra = "\n# HL-F3/F4 consolidation fields (#3345) — splice into existing [memory.hebbian] section:\n\
# consolidation_interval_secs = 3600 # how often the sweep runs (0 = disabled)\n\
# consolidation_threshold = 5.0 # degree × avg_weight score to qualify\n\
# consolidate_provider = \"fast\" # provider name for LLM distillation\n\
# max_candidates_per_sweep = 10\n\
# consolidation_cooldown_secs = 86400 # re-consolidation cooldown per entity\n\
# consolidation_prompt_timeout_secs = 30\n\
# consolidation_max_neighbors = 20\n";
let output = format!("{toml_src}{extra}");
Ok(MigrationResult {
output,
changed_count: 1,
sections_changed: vec!["memory.hebbian".to_owned()],
})
}
pub fn migrate_memory_hebbian_spread_config(
toml_src: &str,
) -> Result<MigrationResult, MigrateError> {
let has_section = toml_src.lines().any(|l| l.trim() == "[memory.hebbian]");
if !has_section {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let has_spreading = toml_src
.lines()
.any(|l| l.trim().starts_with("spreading_activation"));
let has_depth = toml_src
.lines()
.any(|l| l.trim().starts_with("spread_depth"));
let has_budget = toml_src
.lines()
.any(|l| l.trim().starts_with("step_budget_ms"));
let has_embed_timeout = toml_src
.lines()
.any(|l| l.trim().starts_with("embed_timeout_secs"));
if has_spreading && has_depth && has_budget && has_embed_timeout {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let extra = "\n# HL-F5 spreading-activation fields (#3346) — splice into existing [memory.hebbian] section:\n\
# spreading_activation = false # opt-in BFS from top-1 ANN anchor; requires enabled=true\n\
# spread_depth = 2 # BFS hops, clamped [1,6]\n\
# spread_edge_types = [] # MAGMA edge types to traverse; empty = all\n\
# step_budget_ms = 80 # per-step circuit-breaker timeout (anchor ANN / edges / vectors)\n\
# embed_timeout_secs = 5 # timeout for the initial query embedding call (0 = disabled)\n";
let output = format!("{toml_src}{extra}");
Ok(MigrationResult {
output,
changed_count: 1,
sections_changed: vec!["memory.hebbian.spreading_activation".to_owned()],
})
}
pub fn migrate_focus_auto_consolidate_min_window(
toml_src: &str,
) -> Result<MigrationResult, MigrateError> {
if toml_src.contains("auto_consolidate_min_window") {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
if !toml_src.lines().any(|l| l.trim() == "[agent.focus]") {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let comment = "\n# Minimum messages in a low-relevance window before Focus auto-consolidation \
runs (#3313).\n\
# auto_consolidate_min_window = 6\n";
let output = insert_after_section(toml_src, "agent.focus", comment);
Ok(MigrationResult {
output,
changed_count: 1,
sections_changed: vec!["agent.focus.auto_consolidate_min_window".to_owned()],
})
}
pub fn migrate_memory_retrieval_query_bias(
toml_src: &str,
) -> Result<MigrationResult, MigrateError> {
if !toml_src.lines().any(|l| l.trim() == "[memory.retrieval]") {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
if toml_src
.lines()
.any(|l| l.trim().starts_with("query_bias_correction"))
{
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let comment = "\n# MM-F3 (#3341): shift first-person queries toward the user profile centroid.\n\
# No-op when the persona table is empty.\n\
# query_bias_correction = true\n";
let output = insert_after_section(toml_src, "memory.retrieval", comment);
Ok(MigrationResult {
output,
changed_count: 1,
sections_changed: vec!["memory.retrieval.query_bias_correction".to_owned()],
})
}
pub fn migrate_memory_persona_config(toml_src: &str) -> Result<MigrationResult, MigrateError> {
if toml_src
.lines()
.any(|l| l.trim() == "[memory.persona]" || l.trim() == "# [memory.persona]")
{
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let comment = "\n# [memory.persona] — user persona profile for query-bias correction (#3341).\n\
# Verified working in CI-604/CI-605. No-op when disabled.\n\
# [memory.persona]\n\
# enabled = true\n\
# min_messages = 2 # minimum user messages before persona extraction fires\n\
# min_confidence = 0.5 # minimum extraction confidence threshold (0.0–1.0)\n";
let output = format!("{toml_src}{comment}");
Ok(MigrationResult {
output,
changed_count: 1,
sections_changed: vec!["memory.persona".to_owned()],
})
}
pub fn migrate_memory_graph_recall_include_imported(
toml_src: &str,
) -> Result<MigrationResult, MigrateError> {
if !toml_src.lines().any(|l| l.trim() == "[memory.graph]") {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let in_graph_section = {
let mut in_section = false;
toml_src.lines().any(|l| {
let t = l.trim();
if !t.starts_with('#') && t.starts_with('[') && !t.starts_with("[[") {
in_section = t == "[memory.graph]";
return false;
}
if t.starts_with('#') {
let inner = t.trim_start_matches('#').trim();
if inner.starts_with('[') {
in_section = false;
return false;
}
return in_section && inner.starts_with("recall_include_imported");
}
in_section && t.starts_with("recall_include_imported")
})
};
if in_graph_section {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let comment = "\n# Whether to include ingest-origin edges/entities in recall (#5015).\n\
# Set false to isolate conversation knowledge from imported knowledge.\n\
# recall_include_imported = true\n";
let output = insert_after_section(toml_src, "memory.graph", comment);
Ok(MigrationResult {
output,
changed_count: 1,
sections_changed: vec!["memory.graph.recall_include_imported".to_owned()],
})
}
pub fn migrate_qdrant_api_key(toml_src: &str) -> Result<MigrationResult, MigrateError> {
if toml_src.contains("qdrant_api_key") {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let mut doc = toml_src.parse::<toml_edit::DocumentMut>()?;
if !doc.contains_key("memory") {
doc.insert("memory", toml_edit::Item::Table(toml_edit::Table::new()));
}
let comment = "\n# Qdrant API key (optional; required when connecting to remote/managed Qdrant clusters).\n\
# Leave empty for local Qdrant instances. Store the actual key in the vault:\n\
# zeph vault set ZEPH_QDRANT_API_KEY \"<key>\"\n\
# qdrant_api_key = \"\"\n";
let raw = doc.to_string();
let output = format!("{raw}{comment}");
Ok(MigrationResult {
output,
changed_count: 1,
sections_changed: vec!["memory.qdrant_api_key".to_owned()],
})
}
pub fn migrate_qdrant_timeout_secs(toml_src: &str) -> Result<MigrationResult, MigrateError> {
if toml_src.contains("qdrant_timeout_secs") {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let mut doc = toml_src.parse::<toml_edit::DocumentMut>()?;
if !doc.contains_key("memory") {
doc.insert("memory", toml_edit::Item::Table(toml_edit::Table::new()));
}
let comment = "\n# Per-call timeout applied to every Qdrant gRPC operation, in seconds.\n\
# Bounds each call against a hung server or a stalled network path.\n\
# qdrant_timeout_secs = 10\n";
let raw = doc.to_string();
let output = format!("{raw}{comment}");
Ok(MigrationResult {
output,
changed_count: 1,
sections_changed: vec!["memory.qdrant_timeout_secs".to_owned()],
})
}
pub fn migrate_fidelity_timeout_defaults(toml_src: &str) -> Result<MigrationResult, MigrateError> {
let has_embed = toml_src.contains("embed_timeout_secs");
let has_compress = toml_src.contains("compress_timeout_secs");
if (has_embed && has_compress) || !toml_src.contains("[memory.fidelity]") {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let mut output = toml_src.to_owned();
let mut changed = false;
if !has_embed {
let comment = "# embed_timeout_secs = 30 \
# timeout in seconds for embed calls in fidelity scoring\n";
output = output.replacen(
"[memory.fidelity]\n",
&format!("[memory.fidelity]\n{comment}"),
1,
);
changed = true;
}
if !has_compress {
let comment = "# compress_timeout_secs = 30 \
# timeout in seconds for the LLM compress call in fidelity scoring\n";
output = output.replacen(
"[memory.fidelity]\n",
&format!("[memory.fidelity]\n{comment}"),
1,
);
changed = true;
}
if changed {
Ok(MigrationResult {
output,
changed_count: 1,
sections_changed: vec!["memory.fidelity".to_owned()],
})
} else {
Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
})
}
}