Coordinate multiple AI agent sessions working on the same codebase. Agents communicate through a shared SQLite database using the cm CLI.
One-liner (requires go and git):
curl -sSL https://raw.githubusercontent.com/daviddao/clockmail/main/install.sh | shCustom install directory:
curl -sSL https://raw.githubusercontent.com/daviddao/clockmail/main/install.sh | INSTALL_DIR=/usr/local/bin shOr build from source manually:
git clone https://github.com/daviddao/clockmail.git && cd clockmail
go build -ldflags "-X main.version=$(git describe --tags 2>/dev/null || git rev-parse --short HEAD) -X main.commit=$(git rev-parse --short HEAD) -X main.date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" -o cm ./cmd/cmcm init --agent alice # create DB, register yourself, inject AGENTS.md
export CLOCKMAIL_AGENT=alice
cm register bob # register another agent
cm send bob "working on auth.go, don't touch it"
cm send all "status update" # broadcast to all agents
cm broadcast "status update" # same as above
cm lock auth.go # exclusive file lock
# ... do work ...
cm unlock auth.go
cm sync --epoch 1 # heartbeat + receive messages + check frontierEvery event (message, heartbeat, lock request) gets a logical timestamp ts. There is no wall clock — just a counter that establishes causality.
Rules (from Lamport 1978):
- IR1: Before each local event, increment your clock:
ts = ts + 1 - IR2: When sending a message, attach your current
ts. When receiving a message with timestampT, setts = max(ts, T) + 1
This guarantees: if event A causally precedes event B, then ts(A) < ts(B). Two events with no causal link are concurrent — neither happened "before" the other.
Total ordering for conflicts: When two agents request the same lock, the one with the lower (ts, agent_id) wins. The agent ID breaks ties deterministically. This is why cm lock can resolve conflicts without a central coordinator.
Reading cm output: When you see ts=5, that means 5 causal steps have occurred in this agent's history. A message showing [ts=3] alice: ... was sent when alice's clock read 3. If your clock is at 7, you know your current state incorporates alice's message (since receiving it bumped your clock past 3).
Each agent reports a working position as (epoch, round) — called a pointstamp. The frontier is the set of minimum active pointstamps across all agents.
What this answers: "Can I safely assume all agents are done with epoch N?" If every agent has advanced past epoch N, the frontier has moved past it, and it is SAFE to finalize. If any agent is still at or behind epoch N, it is NOT SAFE.
Reading frontier output:
SAFE to finalize epoch=1— all agents moved past epoch 1, no late-arriving work is possibleNOT SAFE ... blocked by bob at epoch=1— bob hasn't advanced yet, wait or coordinate
Use cm frontier --epoch N to check, or cm sync --epoch N which checks automatically.
| Command | What it does |
|---|---|
cm init [--agent ID] |
Create DB, register agent, inject AGENTS.md |
cm onboard |
Print a short primer (for cold-start agents reading AGENTS.md) |
cm prime |
Print full coordination context: your state, peers, locks, frontier |
cm register <id> |
Register a new agent |
cm heartbeat [--epoch N] |
Advance clock, report working position |
cm send <to> <msg> |
Send message (drains inbox first, bidirectional) |
cm broadcast <msg> |
Send to all agents (shorthand for send all) |
cm recv [--summary] |
Receive new messages (cursor-tracked, only shows unread; --summary truncates to 80 chars) |
cm lock <path> |
Acquire exclusive file lock (exit 2 if denied) |
cm unlock <path> |
Release file lock |
cm frontier [--epoch N] |
Check if epoch N is safe to finalize |
cm log |
Show all events in causal order |
cm sync [--epoch N] |
Combined: heartbeat + recv + frontier |
cm watch |
Stream messages (agent mode) or all events (global mode, no agent required) |
cm status |
Overview of all agents, locks, and frontier |
All commands accept --agent <id> (overrides CLOCKMAIL_AGENT) and --json for machine-readable output.
Aliases: hb = heartbeat, ex = send (formerly exchange), exchange = send, broadcast = send all.
Recipients: Use all as the recipient to broadcast to every registered agent (excludes self). Works with both send and exchange.
cm watch without an agent streams all events from all agents in real-time — messages, lock requests, heartbeats, everything. This is a read-only passive observer with no clock side-effects.
cm watch # global stream (no CLOCKMAIL_AGENT needed)
cm watch --all # explicit global even with CLOCKMAIL_AGENT set
cm watch --all --kind msg # global, filtered to messages only
cm watch --kind lock_req # global, filtered to lock activity
cm watch # with CLOCKMAIL_AGENT set: agent inbox (original behavior)The global mode tracks events by row ID rather than Lamport timestamp, so it never misses events that share a timestamp.
| Variable | Default | Purpose |
|---|---|---|
CLOCKMAIL_DB |
.clockmail/clockmail.db |
Path to shared SQLite database |
CLOCKMAIL_AGENT |
(none) | Your agent ID (avoids --agent on every call) |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Error |
| 2 | Lock denied (another agent holds it) |
cm init injects a small section into AGENTS.md that tells new agents to run cm onboard. This follows progressive disclosure:
AGENTS.md --> "run cm onboard"
cm onboard --> who you are, who's active, what to do next
cm prime --> full dynamic context (run this at session start)
cm status --> detailed runtime view
See SKILL.md for agent workflow patterns: when to lock, how to read timestamps, session lifecycle, and conflict resolution.
# Terminal 1: Alice
cm init --agent alice && export CLOCKMAIL_AGENT=alice
cm heartbeat --epoch 1
cm lock auth.go
cm send bob "editing auth.go"
cm broadcast "I'm working on auth.go" # tells all agents
cm unlock auth.go
cm heartbeat --epoch 2
# Terminal 2: Bob
export CLOCKMAIL_AGENT=bob
cm register bob
cm sync --epoch 1
# => receives alice's message, sees frontier is SAFE (alice moved to epoch 2)