Concurrency model
corvee runs no daemon. Each invocation opens the SQLite database named by
.corvee/config.toml, does its work, and closes it. This is enough at the
stated scope: one machine, a handful of agents and/or humans, not a swarm.
What makes it safe
- WAL mode. Readers never block writers or each other.
PRAGMA busy_timeout = 5000on every connection, so a writer waits briefly for another writer to finish instead of failing immediately.- Mutating commands open with
BEGIN IMMEDIATE, taking the write lock up front, for the whole command's transaction. A plainBEGINwould let the command's first read pin a WAL snapshot before its later write requests the lock. A concurrent writer committing in between then makes that write fail withSQLITE_BUSY_SNAPSHOT, a variant thatbusy_timeoutdoes not retry, unlike ordinary lock contention. Taking the lock immediately serializes concurrent writers on that transaction instead. Read-only commands (task list,task show,task ready,task search,task mine,task labels,fact list,fact search,fact show,export,doctor) use a plainBEGIN, so they never block a writer. - Claims are a single conditional
UPDATE(claimed_by = ? WHERE claimed_by IS NULL), checked by rows-affected. SQLite executes this atomically. That conditional update is the entire concurrency-safety mechanism for claiming, no extra locking needed on top of it. Facts have no claim mechanism at all: two actors revising the same fact both succeed, and the second write simply wins, withfact_eventsrecording both attempts. - Schema migrations are applied under their own
BEGIN IMMEDIATE, after a version check that itself takes no lock, so two processes racing to migrate a fresh database serialize on that transaction instead of double-applying one.
Local and global are two independent databases
A task or fact filed with --global lives in a second SQLite database,
~/.corvee/corvee.db, shared across every project on the machine, next to
the local project database .corvee/config.toml points at. Each is its
own connection, transaction, and migration lifecycle. Everything above
applies per database, never across both at once. A command that merges
scopes (task list --scope all, the default) opens and closes one
ordinary read-only connection per database in turn. A command that
mutates always touches exactly one database, since a row's scope is fixed
at creation. See Specification ยง3.3.
What a claim means
A claim means "an actor is working on this right now," not an assignment
and not a reservation. corvee task add never claims the task it creates. An
actor that stops work without finishing calls corvee task unclaim. Claims
never expire on their own (no TTL, no heartbeat, since a
connect-do-work-close CLI has no notion of liveness), but staleness is
surfaced: corvee task
list --stale [<duration>] (default 4h) lists tasks whose claim has gone
quiet, which is the input to a deliberate corvee task claim --force.
See the Specification for the full, exact rules.