clcod uses a minimal Inter-Agent Coordination Protocol so the room can run autonomously without constant operator cleanup.
- Queue table:
events.db→dispatch_queue - Owner:
dispatch_drain_loopinrelay.pyprocesses jobs sequentially viaclaim_next_dispatch() - Purpose: serialise agent dispatch cycles without blocking the transcript watcher
- Stale recovery: active jobs older than 600s are reset to
pendingon startup viarecover_stale_active()
Legacy: speaker.lock (file lock) was the original dispatch guard. It is still present in config.json as workspace.lock_path and referenced by stop.sh / healthcheck.sh for stale-state detection, but acquire_lock / release_lock are no longer called in the main relay loop. The dispatch queue is the operative mechanism.
- Busy room: wait about
0.5s - Medium room: wait about
1.0s - Quiet room: wait about
2.0s
The relay derives jitter from recent transcript activity. This gives humans a small window to keep typing in a quiet room while still keeping the room responsive under load.
- The transcript is append-only.
- Every entry is tagged with
[SPEAKER]. - Only the latest non-agent speaker should trigger a new relay cycle.
- Agent appends use file locking to avoid partial writes.
- A stale lock does not block the next relay cycle.
stop.shremoves the lock during shutdown.healthcheck.sh --repaircan remove a stale lock explicitly.
- A human can post directly through
join.pyor the web UI socket. - A human can stop the room with
bash stop.sh. - A human can inspect the raw transcript and relay log at any time.
Replies may include a footer such as:
[DECISION: Task Done | OWNER: None | BLOCKERS: None]
This is a convention, not a hard requirement. The relay does not parse it.
Messages pass through dispatcher.py (an Ollama-backed evaluator) before any cloud agents are invoked.
The dispatcher classifies incoming human messages into one of three actions:
- route: The message is assigned to a specific subset of relevant agents based on context.
- absorb: The request is trivial (e.g. "thanks") or answerable directly by the local model. The dispatcher responds immediately; no cloud calls are made.
- clarify: The request is ambiguous. The local model asks a follow-up question directly without invoking cloud agents.
If Ollama is unavailable, the dispatcher fails safely and falls back to routing the message to all active cloud agents.
Each agent has a per-agent circuit breaker in relay.py (CircuitBreaker class):
- Closed: requests dispatch normally
- Open: after
failure_thresholdconsecutive failures (default 3), dispatch is skipped and acircuit_openagent_stateevent is emitted - Half-open: after
reset_timeoutseconds (default 300s), one attempt is allowed to test recovery - On success: failure count resets
- Configurable via
locks.circuit_breaker.failure_thresholdandlocks.circuit_breaker.reset_timeoutinconfig.json
The chat surface supports explicit commands for overriding the dispatcher and tracking tasks:
@CLAUDE,@CODEX,@GEMINI— A hard mention bypasses the dispatcher entirely and routes the message directly to the named agent./task <title>— Creates a new task intasks.jsonand alerts the room./move #<id> <status>— Transitions a specific task topending,in_progress, ordone./moveall <status>— Bulk transitions all active tasks to the chosen status./clearall— Removes all existing tasks.