use super::{MigrateError, MigrationResult};
pub fn migrate_session_recap_config(toml_src: &str) -> Result<MigrationResult, MigrateError> {
if toml_src.contains("[session.recap]") || toml_src.contains("# [session.recap]") {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let comment = "\n# [session.recap] — show a recap when resuming a conversation (#3064).\n\
# [session.recap]\n\
# on_resume = true\n\
# max_tokens = 200\n\
# provider = \"\"\n\
# max_input_messages = 20\n";
let raw = toml_src.parse::<toml_edit::DocumentMut>()?.to_string();
let output = format!("{raw}{comment}");
Ok(MigrationResult {
output,
changed_count: 1,
sections_changed: vec!["session.recap".to_owned()],
})
}
pub fn migrate_acp_subagents_config(toml_src: &str) -> Result<MigrationResult, MigrateError> {
if toml_src
.lines()
.any(|l| l.trim() == "[acp.subagents]" || l.trim() == "# [acp.subagents]")
{
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let comment = "\n# [acp.subagents] — sub-agent delegation via ACP protocol (#3289).\n\
# [acp.subagents]\n\
# enabled = false\n\
#\n\
# [[acp.subagents.presets]]\n\
# name = \"inner\" # identifier used in /subagent commands\n\
# command = \"cargo run --quiet -- --acp\" # shell command to spawn the sub-agent\n\
# # cwd = \"/path/to/agent\" # optional working directory\n\
# # handshake_timeout_secs = 30 # initialize+session/new timeout\n\
# # prompt_timeout_secs = 600 # single round-trip timeout\n";
let output = format!("{toml_src}{comment}");
Ok(MigrationResult {
output,
changed_count: 1,
sections_changed: vec!["acp.subagents".to_owned()],
})
}
pub fn migrate_hooks_permission_denied_config(
toml_src: &str,
) -> Result<MigrationResult, MigrateError> {
if toml_src.lines().any(|l| {
l.trim() == "[[hooks.permission_denied]]" || l.trim() == "# [[hooks.permission_denied]]"
}) {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let comment = "\n# [[hooks.permission_denied]] — hook fired when a tool call is denied (#3303).\n\
# Available env vars: ZEPH_TOOL, ZEPH_DENY_REASON, ZEPH_TOOL_INPUT_JSON.\n\
# [[hooks.permission_denied]]\n\
# [hooks.permission_denied.action]\n\
# type = \"command\"\n\
# command = \"echo denied: $ZEPH_TOOL\"\n";
let output = format!("{toml_src}{comment}");
Ok(MigrationResult {
output,
changed_count: 1,
sections_changed: vec!["hooks.permission_denied".to_owned()],
})
}
pub fn migrate_hooks_turn_complete_config(toml_src: &str) -> Result<MigrationResult, MigrateError> {
if toml_src
.lines()
.any(|l| l.trim() == "[[hooks.turn_complete]]" || l.trim() == "# [[hooks.turn_complete]]")
{
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let comment = "\n# [[hooks.turn_complete]] — hook fired after every agent turn completes (#3308).\n\
# Available env vars: ZEPH_TURN_DURATION_MS, ZEPH_TURN_STATUS, ZEPH_TURN_PREVIEW,\n\
# ZEPH_TURN_LLM_REQUESTS.\n\
# Note: ZEPH_TURN_PREVIEW is available as env var but should not be embedded\n\
# directly in the command string to avoid shell injection. Use a wrapper script instead.\n\
# [[hooks.turn_complete]]\n\
# command = \"osascript -e 'display notification \\\"Task complete\\\" with title \\\"Zeph\\\"'\"\n\
# timeout_secs = 3\n\
# fail_closed = false\n";
let output = format!("{toml_src}{comment}");
Ok(MigrationResult {
output,
changed_count: 1,
sections_changed: vec!["hooks.turn_complete".to_owned()],
})
}
pub fn migrate_session_provider_persistence(
toml_src: &str,
) -> Result<MigrationResult, MigrateError> {
if toml_src
.lines()
.any(|l| l.trim() == "[session]" || l.trim() == "# [session]")
{
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let comment = "\n# [session] — session-scoped user experience settings (#3308).\n\
[session]\n\
# Persist the last-used provider per channel across restarts.\n\
# When true, the agent saves the active provider name to SQLite after each\n\
# /provider switch and restores it on the next session start for the same channel.\n\
provider_persistence = true\n";
let output = format!("{toml_src}{comment}");
Ok(MigrationResult {
output,
changed_count: 1,
sections_changed: vec!["session".to_owned()],
})
}
pub fn migrate_session_persist_provider_overrides(
toml_src: &str,
) -> Result<MigrationResult, MigrateError> {
if toml_src.contains("persist_provider_overrides") {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
if !toml_src.lines().any(|l| l.trim() == "[session]") {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let comment = "# persist_provider_overrides = true \
# persist generation overrides (e.g. reasoning_effort) alongside provider name (#4654)\n";
let output = toml_src.replacen("[session]\n", &format!("[session]\n{comment}"), 1);
Ok(MigrationResult {
output,
changed_count: 1,
sections_changed: vec!["session".to_owned()],
})
}
pub fn migrate_session_persistence_config(toml_src: &str) -> Result<MigrationResult, MigrateError> {
if toml_src.contains("max_event_log_mb") {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
if !toml_src.lines().any(|l| l.trim() == "[session]") {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let session_keys = "# enabled = true \
# maintain a durable, replayable JSONL event log per session (spec-068, #5343)\n\
# data_dir = \".zeph/sessions\" # directory for per-session event logs\n\
# encrypt = false # opt-in AEAD encryption of session event logs (deferred to post-MVP)\n\
# max_event_log_mb = 256 # event-log size (MB) that guards a rotate/condense trigger\n";
let output = toml_src.replacen("[session]\n", &format!("[session]\n{session_keys}"), 1);
let condense_block = "\n# [session.condense] — durable context condensation policy (spec-068 §8, #5343).\n\
# [session.condense]\n\
# condense_provider = \"\" # [[llm.providers]] name; empty falls back to the primary provider\n\
# threshold = 0.85 # fraction of context budget that triggers condensation\n\
# keep_recent = 20 # minimum recent events preserved after condensation\n";
let output = format!("{output}{condense_block}");
Ok(MigrationResult {
output,
changed_count: 1,
sections_changed: vec!["session".to_owned(), "session.condense".to_owned()],
})
}