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: a child tool slower than the lease, duplicated through wrap.
//!
//! Testing `EffectFence::heartbeat` in isolation proves the function, not the
//! path. This drives the real proxy over stdio against a real child that
//! takes longer than its lease, and counts executions in the CHILD's own
//! ledger — the only number that cannot be faked by a response.
//!
//! Without the heartbeat the first call's lease lapses mid-flight, the
//! duplicate is admitted, and the child charges twice.

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

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

impl Client {
    fn spawn(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_LEASE_SECS", "1") // child sleeps 3s: lease would lapse mid-call
            .env("EFFECTFENCE_LEDGER", "memory") // this test is about the lease, not the ledger
            .env("SLOW_CHILD_LEDGER", ledger)
            .env("SLOW_CHILD_DELAY", "3")
            .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":"slow-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) {
        self.send(
            &serde_json::json!({"jsonrpc":"2.0","id":id,"method":"tools/call",
            "params":{"name":"slow_charge","arguments":{"order":order}}}),
        );
    }

    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 a_child_slower_than_its_lease_is_still_executed_exactly_once() {
    let ledger = std::env::temp_dir().join(format!("ef-slow-{}.ndjson", std::process::id()));
    let ledger_s = ledger.to_string_lossy().to_string();
    let _ = std::fs::remove_file(&ledger);

    let mut c = Client::spawn(&ledger_s);

    // First call: the child will take 3s, three times its 1s lease.
    c.call(10, "order-777");
    // A duplicate arriving after the lease WOULD have lapsed.
    std::thread::sleep(Duration::from_millis(1500));
    c.call(11, "order-777");

    let first = c.read_id(10);
    let second = c.read_id(11);

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

    assert_eq!(
        executions, 1,
        "the child must charge order-777 exactly once; ledger:\n{runs}\n\
         first={first}\nsecond={second}"
    );
    assert!(
        first["result"]["content"][0]["text"]
            .as_str()
            .unwrap_or_default()
            .contains("order-777"),
        "the original call must still get its real result: {first}"
    );
}