effectfence 0.3.0

Causal concurrency fence for multi-agent tool calls: an intent ledger, OCC read-sets, and atomic CAS domain reservation stop double-execution — same-instant races and late duplicate retries alike. Ships as a library and an MCP server.
Documentation
//! End to end: the proxy restarts between two identical calls.
//!
//! The child counts its own executions in a file, so this proves the fence
//! across a process boundary with a number no response can fake. Without a
//! durable ledger the second proxy has never heard of the first call and the
//! child charges twice.

use std::io::{BufRead, BufReader, Write};
use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio};

struct Client {
    child: Child,
    stdin: ChildStdin,
    stdout: BufReader<ChildStdout>,
}

impl Client {
    fn spawn(child_ledger: &str, fence_ledger: &str) -> Self {
        let exe = env!("CARGO_BIN_EXE_effectfence");
        let fixture = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures/slow_child.py");
        let mut child = Command::new(exe)
            .args(["wrap", "--", "python3", fixture])
            .env("EFFECTFENCE_LEDGER", fence_ledger)
            .env("SLOW_CHILD_LEDGER", child_ledger)
            .env("SLOW_CHILD_DELAY", "0")
            .stdin(Stdio::piped())
            .stdout(Stdio::piped())
            .stderr(Stdio::null())
            .spawn()
            .expect("spawn wrap");
        let stdin = child.stdin.take().unwrap();
        let stdout = BufReader::new(child.stdout.take().unwrap());
        let mut c = Self {
            child,
            stdin,
            stdout,
        };
        c.send(
            &serde_json::json!({"jsonrpc":"2.0","id":1,"method":"initialize","params":{
            "protocolVersion":"2026-07-28","capabilities":{},
            "clientInfo":{"name":"restart-test","version":"0"}}}),
        );
        c.read_id(1);
        c.send(&serde_json::json!({"jsonrpc":"2.0","method":"notifications/initialized"}));
        c
    }

    fn send(&mut self, v: &serde_json::Value) {
        writeln!(self.stdin, "{v}").unwrap();
        self.stdin.flush().unwrap();
    }

    fn call(&mut self, id: i64, order: &str) -> serde_json::Value {
        self.send(
            &serde_json::json!({"jsonrpc":"2.0","id":id,"method":"tools/call",
            "params":{"name":"slow_charge","arguments":{"order":order}}}),
        );
        self.read_id(id)
    }

    fn read_id(&mut self, id: i64) -> serde_json::Value {
        loop {
            let mut line = String::new();
            let n = self.stdout.read_line(&mut line).expect("read");
            assert!(n > 0, "wrap closed stdout while awaiting id {id}");
            if let Ok(v) = serde_json::from_str::<serde_json::Value>(&line)
                && v.get("id").and_then(|x| x.as_i64()) == Some(id)
            {
                return v;
            }
        }
    }
}

impl Drop for Client {
    fn drop(&mut self) {
        let _ = self.child.kill();
        let _ = self.child.wait();
    }
}

#[test]
fn an_identical_call_after_a_proxy_restart_is_replayed_not_rerun() {
    let pid = std::process::id();
    let child_ledger = std::env::temp_dir().join(format!("ef-restart-child-{pid}.ndjson"));
    let fence_ledger = std::env::temp_dir().join(format!("ef-restart-fence-{pid}.jsonl"));
    let _ = std::fs::remove_file(&child_ledger);
    let _ = std::fs::remove_file(&fence_ledger);
    let child_s = child_ledger.to_string_lossy().to_string();
    let fence_s = fence_ledger.to_string_lossy().to_string();

    let first = {
        let mut c = Client::spawn(&child_s, &fence_s);
        c.call(10, "order-restart")
    }; // the proxy is killed here

    let second = {
        let mut c = Client::spawn(&child_s, &fence_s);
        c.call(20, "order-restart")
    };

    let runs = std::fs::read_to_string(&child_ledger).unwrap_or_default();
    let executions = runs.lines().filter(|l| !l.trim().is_empty()).count();
    let _ = std::fs::remove_file(&child_ledger);
    let _ = std::fs::remove_file(&fence_ledger);

    assert_eq!(
        executions, 1,
        "the child must charge order-restart exactly once across a proxy restart; \
         ledger:\n{runs}\nfirst={first}\nsecond={second}"
    );
    assert_eq!(
        second["result"]["content"][0]["text"], first["result"]["content"][0]["text"],
        "the second proxy must hand back the first proxy's recorded result"
    );
}