Task board + scheduler
Task board + scheduler
← index
What this is
Colibri's task board holds operator-submitted work items, and the scheduler
assigns them to the best-fit agent on each tick. Tasks flow in via the
daemon's Unix socket (create-task, intake-task) and are drained by the
scheduler loop running inside the daemon every ~30 seconds.
Decisions
Capability match scoring (best-fit, not first-fit)
When the scheduler picks an agent for a task, it scores every available agent
against the task's required capabilities using a simple intersection count:
|required ∩ agent_caps| / |required|. The agent with the highest score wins;
ties are broken by agent name (deterministic, so repeated runs don't thrash).
A task with ["freebsd", "zfs"] will match an agent with both capabilities
over one with only freebsd. A task with no required capabilities matches
any agent. Offline agents and agents whose capabilities don't intersect at all
are skipped.
Why not round-robin or FIFO: capability-based matching means the right agentgets the right work without operator hand-assignment. The scoring is trivial
(set intersection) and transparent — no machine learning, no weights to tune.
→ crates/colibri-daemon/src/scheduler.rs
(capability_match_score, pick_agent)
Three schedule types (cron, interval, once)
| Type | Behavior |
|---|---|
| Cron | Fires at specific wall-clock times (e.g. 0 0 * = midnight). |
| Interval | Fires after a fixed duration since last run (e.g. 3600s). |
| Once | Fires exactly once, at the specified future time. |
Cron patterns are simple 5-field expressions (minute, hour, day, month,
weekday) with wildcards — no second granularity, no /step syntax. The
matching uses prefix comparison: a cron pattern matches if each field of the
current time begins with the pattern string, so 0 matches 00, 1 matches
10-19, etc. This is intentionally simple — cron is a convenience for periodic
housekeeping, not a general-purpose job engine.
Why not use a real cron library: the scheduler's job is dispatching tasks toagents, not calendar management. The simple prefix-match cron covers 90% of use
cases (daily builds, hourly reports) without pulling in a parsing dependency.
→ crates/colibri-daemon/src/scheduler.rs
(should_fire)
Intake drain (queue → task board → agent)
The intake-task socket command pushes a task onto the intake queue. On each
scheduler tick (~30s), the loop drains the intake queue into the task board's
SQLite store, then checks for due scheduled jobs. This two-phase drain
decouples submission from execution: the operator submits at any time, the
scheduler processes in batches.
Tasks in the intake queue carry a capability string (not an agent ID). The
scheduler picks the best agent at execution time, so a task submitted when no
matching agent is online will be picked up when one connects.
Why an intake queue, not direct assignment: agents come and go. If submissionrequired picking an agent, the operator would need to know which agents are
available — a coupling the task board deliberately avoids.
→ crates/colibri-daemon/src/scheduler.rs
(Scheduler, add_job, submit),
crates/colibri-daemon/tests/intake_scheduler_loop.rs
SQLite backing (embedded, not a service)
The task board stores tasks, agent registrations, tenant info, and the skills
catalog in an embedded SQLite database at /var/db/colibri/colibri.sqlite. No
separate database process — the daemon opens the file directly.
Why SQLite, not PostgreSQL: the daemon runs on the operator USB and ondeployed hosts. A full PostgreSQL service is heavyweight for a single daemon's
coordination state. SQLite is zero-config, zero-admin, and survives daemon
restarts without a separate lifecycle. The mother node uses PostgreSQL for the
hive registry because it's multi-tenant; the local daemon is single-tenant.
→ crates/colibri-store/src/lib.rs
See also
- mother-hive — the mother node's PostgreSQL-based hive registry
- cost-model — cost tracking per session
- agent-harness — autospawn