Locks

Two lock types, deliberately separate: the lease answers “who may write to this brain”, the mutex answers “not at the same time”. Neither substitutes for the other.

The two types

  lib/brain-lock.js lib/lock.js
Concept authorization lease cross-process mutex
Question who may write to this brain? serialize one snapshot write
Lifetime long (TTL default 1h) short (held across one write)
Identity token + agentId none (pid-only, advisory)
Root models/private/.locks/.lock-<brain>.json models/private/<brain>/.locks/<kind>[__<id>].lock
Events brain-lock:acquired\|released\|refreshed\|force-released none
CLI vant lock (library only)
MCP vant_lock (library only)

Why two roots: the lease is a cross-brain registry (one file per brain), so it lives at the shared parent models/private/.locks/. The mutex guards a resource inside one brain, so its files live at models/private/<brain>/.locks/. Both .locks directories are infrastructure dot-dirs: brain loading and migrations ignore them, and they are gitignored.

The lease: lib/brain-lock.js

An authorization lease with a token and a TTL. Acquire it before an agent takes exclusive write ownership of a brain; release it when done.

vant lock acquire              # prints the token, saves it to .lock-brain-token
vant lock status               # current brain + whole-stack view
vant lock release              # token-verified release (refuses a wrong token)
vant lock force                # admin: drop the lock file unconditionally

Programmatic use:

const brainLock = require('./lib/brain-lock');

const token = await brainLock.acquireBrainLock('agent-1');
if (token) {
    try {
        // exclusive brain write ownership ...
    } finally {
        await brainLock.releaseBrainLock('agent-1', token);
    }
}

Behaviour worth knowing:

The lease is not a write serializer. It says nothing about two processes racing to write one file; that is the mutex below.

The mutex: lib/lock.js

A short-lived advisory lockfile that makes one whole-snapshot write atomic across processes. Atomic wx create (O_EXCL), stale takeover for crashed writers (default 5s), symlink guard, and release only by the owning pid (a stale takeover never clobbers the successor’s fresh lock).

const lock = require('./lib/lock');

const p = lock.pathFor('teams');              // models/private/<brain>/.locks/teams.lock
const res = await lock.withLock(p, () => {
    // re-read the disk under the lock, merge, write the whole snapshot
}, { failMode: 'closed', staleMs: 10000, waitMs: 8000 });

Key facts:

Which path helper?

Resource lives at Helper Lock root
Inside one brain (<brain>/state/..., orgchart, habitat) pathFor(kind, id) models/private/<brain>/.locks/
Repo root, shared across brains (.circuit-auth.json, .circuit-vaf.json, models/public/insights.json) pathForGlobal(kind, id) models/.locks-global/

A per-brain lock on a repo-root file would split-brain two processes pinned to different brains, hence the global root. Both roots are gitignored and scanned for leaked files by npm run lint:locks.

Failure postures

Every mutex call site declares its posture explicitly:

If you see Orgchart lock unavailable or a similar fail-closed refusal, the system protected your data: fix the filesystem issue (or the stale peer) and retry. vant health shows lock state under its Lock section.

What is NOT a lock

Three serializers sit in the same conceptual space and are classified so nobody mistakes them:

Thing What it is Why it is not a lock
lock.mutex() in-process promise chain no cross-process guarantee
teams._teamsSaveChain, agents._saveChain in-process write ordering same-process only
lib/recursion.js guard depth/reentrancy guard never requires either lock module

Diagnostics

vant health          # Lock section: layer status + per-brain lease rows
vant lock status     # lease status for the active brain and the whole stack
npm run lint:locks   # audit: two roots, no cross-require, zero leaked lockfiles

Related pages: QoS for rate limiting, Storage for the storage layer the lease rides on, Multi-agent crews for why write ownership exists. The full invariant list lives in labs/LOCKS.md (PRD section 8).