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
//! The gap between "crashed" and "still running".
//!
//! An intent lease is taken over once it expires, which is what makes crash
//! recovery possible. Nothing distinguishes a holder that died from one that
//! is merely slower than its lease — so an effect that outlives `lease_ttl`
//! has its claim stolen and runs a SECOND time. That is the exact
//! double-execution this crate exists to prevent, and until now the only
//! defence was guessing a large enough `lease_ttl` up front.
//!
//! These tests pin the defect and the way out of it.

use effectfence::fence::{
    Admission, EffectFence, EffectRequest, FenceConfig, VectorClock, prepare_effect_fence,
};
use std::time::Duration;

fn req(intent: &str, agent: &str) -> EffectRequest {
    EffectRequest {
        intent: intent.to_string(),
        parent: None,
        domain: "order:9".to_string(),
        tool: "charge".to_string(),
        args: serde_json::json!({"amount": 4900}),
        read_set: vec![],
        agent: agent.to_string(),
        known_clock: VectorClock::new(),
    }
}

fn short_lease() -> EffectFence {
    EffectFence::with_config(FenceConfig {
        lease_ttl: Duration::from_millis(50),
        ..FenceConfig::default()
    })
}

/// The defect, stated as a fact: a holder that is still running loses its
/// claim, and a second attempt is admitted to run the same intent.
#[test]
fn a_slow_holder_that_cannot_signal_life_loses_its_claim() {
    let fence = short_lease();
    let _still_running =
        prepare_effect_fence(&fence, req("charge:slow-1", "agent-a")).expect("first admission");

    std::thread::sleep(Duration::from_millis(120));

    let second = prepare_effect_fence(&fence, req("charge:slow-1", "agent-b"));
    assert!(
        matches!(second, Ok(Admission::Fresh(_))),
        "documents the hazard: with no way to signal life, the slow holder's \
         intent is admitted a second time"
    );
}

/// The way out: a holder that says it is alive keeps its claim.
#[test]
fn heartbeat_keeps_a_slow_holder_s_claim() {
    let fence = short_lease();
    let _running =
        prepare_effect_fence(&fence, req("charge:slow-2", "agent-a")).expect("first admission");

    // Three beats spanning well past the original 50 ms lease.
    for _ in 0..3 {
        std::thread::sleep(Duration::from_millis(30));
        assert!(
            fence.heartbeat("charge:slow-2"),
            "an in-flight intent must accept a heartbeat"
        );
    }

    let second = prepare_effect_fence(&fence, req("charge:slow-2", "agent-b"));
    assert!(
        matches!(
            second,
            Err(effectfence::fence::FenceError::IntentInFlight { .. })
        ),
        "a heartbeating holder keeps its claim; got {second:?}"
    );
}

/// A heartbeat must never resurrect or invent a claim: it reports whether
/// this intent is still in flight, and answers `false` when it is not.
#[test]
fn heartbeat_refuses_anything_that_is_not_in_flight() {
    let fence = short_lease();
    assert!(!fence.heartbeat("charge:never-existed"), "unknown intent");

    let prepared = match prepare_effect_fence(&fence, req("charge:slow-3", "agent-a")).unwrap() {
        Admission::Fresh(p) => p,
        other => panic!("expected a fresh admission, got {other:?}"),
    };
    effectfence::fence::commit_effect_cert(&fence, prepared, serde_json::json!({"ok": true}))
        .expect("commit");
    assert!(
        !fence.heartbeat("charge:slow-3"),
        "a committed intent is finished, not in flight — extending it would \
         reopen a settled outcome"
    );

    // A lease already lost to a takeover belongs to the new holder.
    let _lost = prepare_effect_fence(&fence, req("charge:slow-4", "agent-a")).unwrap();
    std::thread::sleep(Duration::from_millis(120));
    let _taken = prepare_effect_fence(&fence, req("charge:slow-4", "agent-b")).unwrap();
    assert!(
        fence.heartbeat("charge:slow-4"),
        "the CURRENT holder may beat (the fence tracks the lease, not who holds it)"
    );
}