Level 5: Consequences survive restarts
You'll learn why restarting a process never resets an agent.
Run cargo run --example mastery_05_persistence, three times in a row
The idea
The guard keeps no state of its own. Guard::open verifies the agent’s log, and replays it: spent budget, scars, pending approvals and termination all come back exactly as they were. The only way to “start over” is to create a new agent, with a new identity and an empty history.
The code
//! Lineage Mastery, level 5: consequences survive restarts.
//!
//! The guard keeps no state of its own. `Guard::open` verifies the agent's log and
//! replays it, so spent budget, scars, pending approvals, and termination all carry over.
//! Run this example several times in a row and watch the agent age.
//!
//! Run: cargo run --example mastery_05_persistence (again and again)
//! Reset: cargo run --example mastery_05_persistence -- --reset
use std::path::Path;
use lineage::audit::AuditKey;
use lineage::guard::{Decision, Guard, Outcome, Policy, ToolRule};
use serde_json::json;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let dir = Path::new("mastery-data/05");
if std::env::args().any(|a| a == "--reset") {
let _ = std::fs::remove_dir_all(dir);
println!("reset: the agent is gone. Its history is not 'undone': it is simply a new agent next time.");
return Ok(());
}
let log = dir.join("agent.jsonl");
let key = AuditKey::load_or_create(dir.join("audit.key"))?;
// Open the agent if it exists (replaying its history), otherwise create it.
let mut guard = if log.exists() {
Guard::open(&log, key)?
} else {
println!("(first run: creating the agent)");
Guard::create(&log, key, "night-shift", Policy::new(20).allow("work", ToolRule::cost(3)).scar_limit(6))?
};
let before = guard.status();
println!("start of run: spent {}/{}, scars {}/{}, alive {}", before.spent, before.budget,
before.scar_score, before.scar_limit, before.alive);
// Each run does some work and makes one mistake.
for (tool, input) in [("work", json!({"task": "reconcile"})), ("wipe_disk", json!({"disk": "/dev/sda"}))] {
match guard.request(tool, input, None)? {
Decision::Allowed { action_id, .. } => {
guard.report(&action_id, Outcome::success(""))?;
println!(" {tool:<9} allowed");
}
Decision::Denied { reason, .. } => println!(" {tool:<9} denied: {reason}"),
Decision::PendingApproval { .. } => unreachable!(),
}
}
let after = guard.status();
println!("end of run: spent {}/{}, scars {}/{}, alive {} ({} records in the log)", after.spent, after.budget,
after.scar_score, after.scar_limit, after.alive, guard.records()?.len());
if !after.alive {
println!("\nThe agent is terminated: {}. Restarting won't change that.", after.termination_reason.unwrap_or_default());
println!("Start over with --reset (which creates a new agent, not a revived one).");
}
Ok(())
}
Run it (three times)
(first run: creating the agent)
start of run: spent 0/20, scars 0/6, alive true
work allowed
wipe_disk denied: tool 'wipe_disk' is not allowed
end of run: spent 3/20, scars 3/6, alive true (8 records in the log)
start of run: spent 3/20, scars 3/6, alive true
work allowed
wipe_disk denied: tool 'wipe_disk' is not allowed
end of run: spent 6/20, scars 6/6, alive false (15 records in the log)
The agent is terminated: scar limit reached (6 >= 6). Restarting won't change that.
start of run: spent 6/20, scars 6/6, alive false
work denied: agent is terminated: scar limit reached (6 >= 6)
wipe_disk denied: agent is terminated: scar limit reached (6 >= 6)
end of run: spent 6/20, scars 6/6, alive false (19 records in the log)
What happened
Each run picked up exactly where the last one ended. The second run crossed the scar limit, and the third could do nothing, even though its process had never seen a scar. The log grew anyway, because denied attempts are part of the record.
Try this
After the agent is terminated, try to bring it back by editing mastery-data/05/agent.jsonl:
- Change a record, say a
"cost":3to"cost":0, and run again.Guard::openrefuses the log:Error: Audit(Corrupt(VerifyFailure { line: Some(2), seq: Some(1), reason: "hash does not match record content" })) - Delete the last line, the
terminatedrecord. What’s left is a valid, shorter chain. But the guard sees the scar limit already reached, with no termination recorded, so it terminates the agent again before it can act:work denied: agent is terminated: scar limit reached (6 >= 6); termination record missing from the log - Cut off more, the scars as well, and you’re truncating history. Only a published checkpoint catches that; see level 6.
Run with -- --reset to start a new agent.