Kibisis · agent memory and leases

Memory for AI agents: local, encrypted, one writer at a time.

Kibisis keeps what an agent learns in one SQLite file on your own disk, each entry sealed with AES-256-GCM, and decides what comes back by scope, trust state and date. When agents share memory, it lets one writer in at a time and numbers every grant.

Try it

Three agents, one resource.

Acquire as A, then try B. Skip ahead past A's deadline and try B again: the token goes up, and A's renew with its old token is refused.

Running kibisis 0.1.0-alpha.5 in your browser (WebAssembly). Loads 73 KB when you start it; nothing is sent anywhere.

Three agents share one resource, repo:main. Each lease asks for 3 seconds; the crate rounds deadlines up to whole seconds. The demo supplies the crate's clock, so you can skip ahead instead of waiting. Demo clock: +0 s

Agent A · no lease
Agent B · no lease
Agent C · no lease

What an agent keeps, and what comes back

Each memory in the Python vault has a category, a key, content and a scope, and every write is versioned. Recall is deliberate:

  • Scopes. Every memory belongs to a scope and carries an HMAC-SHA256 signature over its scope, category, key and content. Search skips, and a direct read refuses, a row whose signature does not verify.
  • Trust states. A memory is a candidate, verified, disputed or revoked. Search leaves out candidates and revoked memories unless asked, and a review records who promoted or revoked what. A write that names no state passes an admission gate, which stores speculation and passing remarks as candidates.
  • Validity windows. A memory can carry valid-from and valid-until times, and search can return what was valid at a given moment. By default, search leaves out memories outside their window.
  • Supersession. A new memory can replace an old one: the old one is revoked, its validity ends, and the two are linked. Each replacement is journaled.
  • Forgetting. A tombstone records a value's digest and deletes every copy of it in the scope. Writing the value again, or a look-alike spelling of it, is refused under any category or key in the scope, and the refusal is journaled. Forgetting through the MCP server also rebuilds the search index, truncates the write-ahead log and vacuums the file.
  • Audit journal. Every put, archive, tombstone and purge appends a hash-chained event in the same transaction as the change, and the chain can be re-checked.
  • For agents. kibisis-mcp (or python -m kibisis.mcp_server) serves remember, recall, forget, verify-ledger and health tools over stdio, for one scope fixed when it starts, and will not start without a key unless plaintext is allowed explicitly. What an agent writes through it is stored as a candidate; a person approves, disputes or rejects it with kibisis-review.

Lease coordination

acquire(resource, holder, ttl) grants a lease or refuses with the current holder's name. renew extends it, release gives it up; both succeed only for the exact lease that was granted. Every grant, including a holder taking the same resource again, gets a fencing token one higher than the last. A holder that pauses past its deadline loses the lease, and any write it attempts later carries an old token that the receiving side can refuse. The Python vault refuses such writes itself when it is opened with a lease coordinator and enforce_fencing=True: a write with a missing, stale or expired token raises LeaseConflictError and changes nothing.

The Rust crate's coordinator is in memory and coordinates threads in one process; deadlines are whole seconds, rounded up, so a lease lasts at least its TTL. The Python package's SQLiteLeaseCoordinator coordinates separate processes through one database file.

Encryption at rest (Python package)

The Python vault keeps memory in one SQLite file. Each entry's content is sealed with AES-256-GCM using a key derived with PBKDF2-HMAC-SHA256 at 600,000 iterations, and an on-disk vault will not open without a passphrase unless you allow plaintext explicitly. With the sqlcipher3 module the package declares, the whole file is also encrypted page by page, so keys and categories are unreadable too.

The Rust crate's vault is in memory in 0.1.0-alpha.5, and the crate has no encryption code. The encrypted, on-disk vault is the Python package.

Install

Rust: Cargo.toml
[dependencies]
kibisis = "=0.1.0-alpha.5"
Python
pip install "kibisis==0.1.0a5"

Version 0.1.0-alpha.5 is on crates.io and PyPI.

Rust (tested)
use kibisis::{InMemoryLeaseCoordinator, LeaseCoordinator};
use std::time::Duration;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut leases = InMemoryLeaseCoordinator::new();
    let a = leases.acquire("repo:main", "agent-a", Duration::from_secs(30))?;
    println!("agent-a holds token {}", a.fencing_token);
    match leases.acquire("repo:main", "agent-b", Duration::from_secs(30)) {
        Ok(_) => println!("agent-b also got it (should not happen)"),
        Err(e) => println!("agent-b refused: {e}"),
    }
    leases.release(&a)?;
    let b = leases.acquire("repo:main", "agent-b", Duration::from_secs(30))?;
    println!("agent-b holds token {}", b.fencing_token); // higher than agent-a's
    // A late write from agent-a carries the old token; check tokens at the point of write.
    println!("renew with agent-a's old token accepted: {}", leases.renew(&a, Duration::from_secs(30))?);
    Ok(())
}
Python (tested)
from kibisis import SQLiteLeaseCoordinator, SQLiteMemoryVault

# One encrypted database file; the passphrase never leaves this process.
vault = SQLiteMemoryVault("memory.db", passphrase="use-a-long-random-passphrase")
leases = SQLiteLeaseCoordinator("memory.db", passphrase="use-a-long-random-passphrase")

lease = leases.acquire("repo:main", "agent-a", ttl_seconds=30.0)
vault.put("decisions", "deploys", "Blue-green deploys since March.")
leases.release(lease)
print("stored under fencing token", lease.fencing_token)

Contention measurements

Rust crate, threads in one process
MeasurementMedianRangeRaw data
Lease operations per second, 16 threads
16 threads, one shared lease, in-memory coordinator in one process. The benchmark asks for a 50 ms lease; the coordinator stores deadlines in whole seconds and rounds up, so a lease lasts at least its TTL and at most one second longer
1,608,796 ops/s1,534,545–1,881,954run1 run2 run3 run4 run5 · source · details
Lease operations per second, 32 threads
32 threads, one shared lease, in-memory coordinator in one process. The benchmark asks for a 50 ms lease; the coordinator stores deadlines in whole seconds and rounds up, so a lease lasts at least its TTL and at most one second longer
1,813,258 ops/s1,465,492–1,856,764run1 run2 run3 run4 run5 · source · details
Contended lease attempts checked
16-thread and 32-thread runs combined
64,000 attemptssame in every runrun1 run2 run3 run4 run5 · source · details
Times two holders had the lease at once
16-thread and 32-thread runs combined; counted by an independent check in the benchmark
0 eventssame in every runrun1 run2 run3 run4 run5 · source · details
Fencing tokens that went backwards
16-thread and 32-thread runs combined
0 eventssame in every runrun1 run2 run3 run4 run5 · source · details
Vault writes per second (in memory)
10,000 writes to the Rust in-memory vault, each hashed into its audit journal; no disk and no encryption in this measurement
338,841 ops/s315,304–342,238run1 run2 run3 run4 run5 · source · details

Python package, separate OS processes, one SQLite file
MeasurementMedianRangeRaw data
Lease operations across separate processes
2, 4, 8, 16 and 32 OS processes sharing one SQLite lease database (Python package)
1,900 operationssame in every runrun1 run2 run3 run4 run5 · source · details
Times two processes held the lease at once
All five process counts; overlaps checked from every process's recorded hold intervals
0 eventssame in every runrun1 run2 run3 run4 run5 · source · details
Fencing tokens that went backwards across processes
All five process counts
0 eventssame in every runrun1 run2 run3 run4 run5 · source · details
Late write from a paused holder rejected
Holder pauses past its TTL (simulated with a sleep); a second process gets a higher token; a sink that compares tokens refuses the first holder's late write
yessame in every runrun1 run2 run3 run4 run5 · source · details
Lease database intact after its holder is killed with SIGKILL
A child process takes the lease and is killed with SIGKILL while holding it. After the lease's TTL, another process opens the database, PRAGMA integrity_check returns ok, and the lease is granted again with the next fencing token
yessame in every runrun1 run2 run3 run4 run5 · source · details

How these numbers were measured. Each figure is the median of its runs, with the range of all runs beside it. Figures from the telarch-benchmarks programs: 5 runs at release tag v0.1.0-alpha.4 on 13th Gen Intel Core i5-13500, 20 logical CPUs, Linux 6.18, rustc 1.97.1, pinned to the performance-core threads (CPUs 0-11). Shared server, not isolated; each run started only after the 1-minute load average was below 4 on two checks 20 s apart. Python multi-process figures: 5 runs of gap3_kibisis_multiproc_crashes.py from telarch-benchmarks commit 83f5ecd64ecc, against the kibisis Python package at v0.1.0-alpha.4, on 13th Gen Intel Core i5-13500, 20 logical CPUs, Linux 6.18, Python 3.12.13, pinned to the performance-core threads (CPUs 0-11). Shared server, not isolated; each run started only after the 1-minute load average was below 4 on two checks 20 s apart. Synthetic data; real workloads will differ. Raw output of every run.

Limits, stated plainly

  • Tokens are only useful if checked. The Python vault checks them when opened with enforce_fencing=True; the Rust vault does not. Any other database, file or API you write to must compare them and refuse older ones.
  • Not a network lock service. The Rust coordinator works inside one process; the Python coordinator works across processes that share one file system.
  • Clock-based deadlines. Expiry uses the local clock. A large clock jump changes when leases expire.
  • The passphrase is the key. Encryption protects a copied file, not a running process that already holds the key.
  • Whole-file encryption needs SQLCipher. The package declares the sqlcipher3 module; if it is missing, the vault still opens with plain SQLite pages and only entry content stays encrypted. Kibisis logs a warning when that happens.
  • Scopes are named by the caller. Kibisis keeps scopes apart; it does not decide which caller may use which scope. The MCP server serves only the scope it was started with.