Preskoฤi na vsebino

Hive Member Tracking & Cost-Aware Routing

Status: Partially shipped โ€” capability matching + eval-driven model selection live (#285); stable node UUID and hive-level cost aggregation pending. Date: 24.jun.2026 Driven by: T1.5 per-task cost tracking (shipped) โ†’ T2.x routing

Companion doc: hive-pane โ€” the operator presentation layer this engine feeds. Hive Pane is the โ€œhow it looksโ€ (board, A2A discovery); this doc is the โ€œhow it worksโ€ (identity, probes, routing, scheduling).

ComponentStateGap
mother_schema.sqlhive_nodes table with hw_profile + capabilities JSONBNo stable node UUID; hostname is the key
derive_capabilities() triggerAuto-computes has_gpu, gpu_vendor, can_run_local_llm, max_model from hw_profileOnly GPU/VRAM heuristics โ€” doesnโ€™t probe running services
clawdie-system-probeCollects GPU, RAM, CPU, disks, ZFS, WiFi, Vulkan, Colibri statusNo ollama/llama.cpp probing
node-register-mcpUPSERTs hw_profile into hive_nodes on join โ€” reachable over the bridge since node_register was allowlisted (#325, see ssh-bridge)No UUID generation at join time
crates/colibri-daemon/src/scheduler.rsCron/interval/one-shot jobs, capability matching (pick_agent), eval-driven select_modelSelection is per-host; no cross-hive awareness yet
colibri-ledgerLocal SQLite agents table with UUID (v4 random)UUID is session-local, not hive-stable
T1.5 cost trackingPer-task cost captured in local SQLiteNo hive-level cost aggregation
  1. Stable identity โ€” A node that joins, leaves, and rejoins is the same node. Not hostname-based (hostnames change when re-provisioned).
  2. Capability matrix โ€” What can each member do? Not just hardware, but running services: ollama, llama.cpp, available models, provider API keys, cost tier.
  3. Verify, donโ€™t guess โ€” Every capability in the matrix comes from a probe result, not self-declaration. The hw-probe is the single source of truth; the derive_capabilities() trigger maps hardware facts โ†’ capability booleans.
  4. Cost-aware routing โ€” When a task is dispatched, the scheduler considers: urgency, provider cost, local LLM availability, cache-hit potential, and capability match.
  5. Local LLM tier โ€” A beefy member can serve as a โ€œfree but slowโ€ execution target for non-urgent tasks. The cost model treats local execution as $0.0000/task.
  6. Extensible โ€” New backends (ollama, llama.cpp, vLLM, Exo clusters) slot into the same capability matrix without schema changes.

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ MOTHER (osa) โ”‚
โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
โ”‚ โ”‚ PostgreSQL โ”‚ โ”‚ Scheduler โ”‚ โ”‚ MCP Bridge โ”‚ โ”‚
โ”‚ โ”‚ hive_nodes โ”‚ โ”‚ cost-aware โ”‚ โ”‚ colibri-mcp-ssh โ”‚ โ”‚
โ”‚ โ”‚ capabilities โ”‚ โ”‚ routing โ”‚ โ”‚ node-register โ”‚ โ”‚
โ”‚ โ”‚ cost_history โ”‚ โ”‚ dispatch โ”‚ โ”‚ cost-query โ”‚ โ”‚
โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
โ–ฒ โ”‚
โ”‚ hw-probe + capabilities โ”‚ task dispatch
โ”‚ (MCP tools/call) โ”‚ (MCP or direct)
โ”‚ โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Node: clawdie-a โ”‚ โ”‚ Node: clawdie-b โ”‚
โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
โ”‚ โ”‚ Colibri โ”‚ โ”‚ โ”‚ โ”‚ Colibri โ”‚ โ”‚
โ”‚ โ”‚ zot spawner โ”‚ โ”‚ โ”‚ โ”‚ zot spawner โ”‚ โ”‚
โ”‚ โ”‚ local SQLiteโ”‚ โ”‚ โ”‚ โ”‚ ollama โ”‚ โ”‚
โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ llama.cpp โ”‚ โ”‚
โ”‚ GPU: none โ”‚ โ”‚ โ”‚ models: โ”‚ โ”‚
โ”‚ RAM: 8GB โ”‚ โ”‚ โ”‚ qwen2.5:7b โ”‚ โ”‚
โ”‚ Cost: cloud-only โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
โ”‚ โ”‚ โ”‚ GPU: RTX 4090 โ”‚
โ”‚ โ”‚ โ”‚ RAM: 64GB โ”‚
โ”‚ โ”‚ โ”‚ Cost: $0 local โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Hostname is unstable. A live-USB node gets clawdie on every boot. A disk-installed node keeps the hostname the operator set. Mother needs a stable, verifiable identity that survives re-provisioning.

A 32-character hex UUID generated once, stored locally, included in every hw-probe report. Like systemdโ€™s /etc/machine-id but simpler: only one purpose โ€” hive identity.

# Generated once by clawdie-firstboot or operator session
/var/db/machine-id โ†’ a1b2c3d4-e5f6-7890-abcd-ef1234567890

Properties:

  • Stable across reboots: stored on disk, not tmpfs
  • Survives re-provisioning: if the seed partition preserves /var/db/machine-id, the same physical machine keeps the same identity
  • Not a secret: itโ€™s an ID, not a key
  • Verifiable: mother can check โ€œhas node a1b2c3d4 ever joined?โ€ โ€” if yes, this is a rejoin, not a new node

Alternatives considered:

ApproachProsCons
SMBIOS UUID (hw.uuid)Truly hardware-bound, survives OS reinstallNot available on all platforms (VPS, ARM); can be spoofed
SSH host key fingerprintCryptographically strongChanges on OS reinstall; key rotation breaks identity
Random UUID (this design)Portable, simple, survives seed restoreCan be copied/cloned (but same machine, same ID โ€” thatโ€™s correct)

Recommendation: Generate on first boot, store in /var/db/machine-id. The hw-probe includes it as machine_id. Motherโ€™s hive_nodes table gets a UNIQUE constraint on machine_id.

ALTER TABLE hive_nodes ADD COLUMN machine_id TEXT;
ALTER TABLE hive_nodes ADD CONSTRAINT uq_machine_id UNIQUE (machine_id);

The node-register-mcp UPSERT switches from ON CONFLICT (hostname) to ON CONFLICT (machine_id). Hostname becomes a mutable attribute (updates on rejoin), machine_id becomes the stable key.


Every capability is a boolean derived from hardware facts, not a self-declaration. The hw-probe collects hardware; the trigger derives capabilities.

CapabilityDerived fromUsed for
has_gpuGPU detected in pciconfGPU-accelerated inference
gpu_vendoramdgpu/nvidia driverModel compatibility
vulkan_computevulkaninfo successllama.cpp Vulkan backend
can_run_local_llmRAM โ‰ฅ 16GB or has GPUEligibility for local task execution
max_modelRAM heuristicModel size limit (3b, 7b-q4, 13b-q4, 34b-q4)
cpu_onlyNo GPU detectedFallback only (slow)
has_wifiwlan devicesNetwork capability
has_zfsZFS pools non-emptyStorage capability
colibri_runningservice statusAgent host eligibility
provider_api_keysMCP-reported (not hw probe)Cloud provider availability

Extend the hw-probe to detect running local LLM services and extend the trigger to derive capabilities from them:

{
"local_llm": {
"ollama_running": true,
"ollama_models": ["qwen2.5:7b", "deepseek-r1:8b", "nomic-embed-text"],
"llama_cpp_installed": true,
"llama_cpp_models": ["/var/db/models/qwen2.5-7b-q4.gguf"],
"vulkan_support": true
}
}

New derived capabilities:

CapabilityDerivation
ollama_availableollama_running == true
ollama_modelsArray of model tags (from ollama list)
llama_cpp_availableBinary at /usr/local/bin/llama-server or similar
llama_cpp_modelsGGUFs in /var/db/models/ or /usr/local/share/models/
can_embed_locallynomic-embed-text in ollama OR any embedding model loaded
inference_tierlocal-fast (GPU โ‰ฅ 24GB), local-slow (CPU-only, RAM โ‰ฅ 16GB), cloud-only
Terminal window
# New collectors
collect_machine_id() # cat /var/db/machine-id or generate
collect_ollama_status() # ollama list 2>/dev/null (JSON models)
collect_llama_cpp() # which llama-server; ls /var/db/models/*.gguf
collect_local_llm() # aggregate JSON block

When a task arrives at the scheduler, it computes a routing score for each eligible node:

score = capability_match ร— urgency_weight ร— cost_weight ร— cache_weight
capability_match: 0.0โ€“1.0 (does the node have the required skills/model?)
cost_weight: 0.0โ€“1.0 (lower cost โ†’ higher weight)
urgency_weight: 0.0โ€“1.0 (time-critical tasks penalize slow nodes)
cache_weight: 0.0โ€“1.0 (warm cache โ†’ higher weight)
TierProviderCost per 1M tokensLatencyUsed when
T0 (free)Local ollama/llama.cpp$0.005โ€“60sNon-urgent, capability match
T1 (cheap)DeepSeek V3$0.27 / $1.102โ€“5sDefault for most tasks
T2 (balanced)Gemini Flash$0.15 / $0.601โ€“3sHigh cache-hit tasks
T3 (premium)Claude Sonnet 4$3.00 / $15.003โ€“8sComplex reasoning, only when needed
  1. If task is non-urgent AND a hive member has a matching local model โ†’ route locally at $0.00 cost.
  2. If the local model is unavailable (node down, model not loaded) โ†’ fall back to T1 (DeepSeek).
  3. If task is urgent (latency < 5s required) โ†’ skip local tier, go straight to T1.
  4. Embedding tasks (RAG, similarity search) โ†’ always prefer local if available. Embeddings are cheap to compute locally and donโ€™t need reasoning.

The scheduler queries hive_nodes for all online nodes, filters by capabilities.can_run_local_llm, checks ollama_models for the required model, and computes the routing score. If no local node matches, it falls back to the cloud tier.

The task schema gets a new field:

ALTER TABLE tasks ADD COLUMN routing JSONB;
-- {"preferred_tier": "local", "allowed_tiers": ["local", "cheap"], "max_cost_usd": 0.05, "deadline_s": null}

A2A integration: See hive-pane for the Agent Card, task exchange, and typed cost data parts. The routing engine (this doc) and the A2A protocol (hive-pane) are orthogonal layers โ€” either can evolve independently.

Node boots โ†’ clawdie-system-probe runs โ†’ MCP tools/call node_register
โ†’ mother UPSERTs hive_nodes โ†’ derive_capabilities() trigger fires
โ†’ capabilities JSONB updated โ†’ node is "online"

New: machine_id is included. If the machine_id already exists, mother updates the existing row (rejoin), preserving history.

colibri-daemon sends periodic heartbeat via MCP. Updates last_seen. If heartbeat misses for > 5 minutes, node status โ†’ offline.

On heartbeat, the node can optionally push updated capabilities (if ollama was installed, models changed, etc.). The hw-probe is re-run and the new local_llm block is sent.

Scheduler picks a queued task
โ†’ queries hive_nodes for eligible nodes
โ†’ computes routing score for each
โ†’ picks winner
โ†’ dispatches task via MCP or direct agent spawn
โ†’ writes routing decision to task.routing JSONB

When the task completes, the local daemon writes cost to its SQLite (T1.5). The mother aggregates hive cost via periodic MCP queries or push events.


What: Mother is the brain. Nodes register, mother routes. No peer-to-peer.

Implementation:

  1. Add machine_id to hive_nodes + hw-probe (1 day)
  2. Extend derive_capabilities() for local LLM (1 day)
  3. Add routing_score() function to motherโ€™s PostgreSQL (stored function โ€” zero Rust changes)
  4. Extend node-register-mcp to accept local_llm block (0.5 day)
  5. Add local_llm probe to clawdie-system-probe (1 day)

Rust changes: Scheduler reads capabilities from hive_nodes via MCP query, computes score, dispatches. ~200 lines.

Total: ~3.5 days.

Pros:

  • Simple to reason about โ€” one source of truth
  • Lowest implementation risk
  • Scheduler lives on mother (always-on)
  • Existing MCP bridge handles all communication

Cons:

  • Mother is single point of failure for routing (but not execution โ€” once dispatched, the task runs independently)
  • Latency: scheduler must query mother on every tick
  • Doesnโ€™t scale to 100+ nodes (not a real concern for our use case)

What: Mother stores the matrix, but nodes can also route tasks they own to peers directly. Hybrid: central registry + distributed execution.

Implementation:

  1. All of Option A (3.5 days)
  2. Add capabilities API to colibri-daemonโ€™s Unix socket (self-awareness) โ€” 1 day
  3. Add local peer discovery via mDNS or Tailscale whois โ€” 1 day
  4. Add direct peer-to-peer task dispatch via Unix socket โ†’ MCP โ†’ remote โ€” 2 days
  5. Add fallback logic: โ€œtry local first, if no response in 30s, ask motherโ€ โ€” 1 day

Total: ~8.5 days.

Pros:

  • Lower latency for local dispatch
  • Survives mother downtime for peer-to-peer tasks
  • Natural fit for local LLM use case (beefy node is on same LAN)
  • Nodes that discover each other can route without phoning home

Cons:

  • Complexity: two code paths (central + peer-to-peer)
  • Security: peer-to-peer dispatch needs authentication (who can send tasks to my daemon?)
  • Harder to audit: cost tracking must handle peer-dispatched vs mother-dispatched tasks differently
  • mDNS doesnโ€™t work across subnets (Tailscale works but adds dependency)

What: Donโ€™t build a routing engine at all. The capability matrix is exposed as an MCP tool that agents query. The agent itself decides where to route based on the matrix + its own reasoning. The matrix is advisory, not prescriptive.

Implementation:

  1. All of Option A minus the routing_scoring function (2.5 days)
  2. Add colibri_query_hive_capabilities MCP tool on mother โ€” returns full online node matrix (0.5 day)
  3. Add colibri_dispatch_to_node MCP tool โ€” sends task to a specific node (1 day)
  4. Write a hive-routing skill that teaches agents how to use the matrix (0.5 day)

Total: ~4.5 days. Zero scheduler changes.

Pros:

  • Exploits Colibriโ€™s architecture-as-differentiator: the agent IS the intelligence
  • The routing decision is auditable in the conversation log (why did the agent pick this node?)
  • Natural fit for local LLM โ€” the agent can reason โ€œthis task is low priority, Iโ€™ll try the beefy node firstโ€
  • No new scheduler code โ€” just MCP tools + skills
  • The skill can be iterated without recompiling Colibri

Cons:

  • Each routing decision costs tokens (the agent must reason about it)
  • Agents make inscrutable routing choices (the LLM โ€œjust knowsโ€)
  • No hard guarantees โ€” an agent might route a $5 task to Claude when DeepSeek would do fine
  • Requires the agent to be โ€œcost-awareโ€ (which requires the MCP cost query tool โ€” already shipping in T1.5)

Start with Option A (Mother-Centric) as the foundation, then layer Option C (Skill-Based) on top.

The capability matrix, stable UUIDs, and local LLM probes are the foundation โ€” theyโ€™re needed regardless of the routing strategy. Option A gives us the data model and probe infrastructure. Once thatโ€™s solid, adding the MCP tools for agent-driven routing (Option C) is a thin layer on top. Option B (peer-to-peer) adds complexity we donโ€™t need at this scale.

Phase 1 (this sprint): Machine UUID + local LLM probes + extended capabilities matrix. ~2.5 days. Phase 2 (next sprint): Mother-based routing + MCP tools for agent-driven routing. ~2 days. Phase 3 (future): Peer-to-peer fallback if we ever have >20 nodes.


DeliverableWhereLines
machine_id generation in clawdie-firstbootclawdie-iso~15
collect_machine_id() in hw-probeclawdie-iso~10
collect_ollama_status() in hw-probeclawdie-iso~30
collect_llama_cpp() in hw-probeclawdie-iso~20
collect_local_llm() aggregator in hw-probeclawdie-iso~25
machine_id column + constraint in mother_schema.sqlcolibri~5
Extended derive_capabilities() for ollama_available, llama_cpp_available, inference_tiercolibri~40
node-register-mcp handling of machine_id key + new local_llm fieldscolibri~15
This design doc (hive-routing.md)This file~0 (done)
DeliverableWhere
colibri_query_hive_capabilities MCP toolcolibri-mcp
colibri_dispatch_to_node MCP toolcolibri-mcp
hive-routing skill.agent/skills/
Task.routing JSONB field in colibri-ledgercolibri-ledger
Mother-side routing score as PostgreSQL function (optional โ€” only if agent-driven routing proves insufficient)mother_schema.sql

The hive routing plan completes the trifectaโ€™s T2.x vision:

T1.4 Prompt Discipline โœ… Cache warming, cost mode, 3-region prompt
T1.5 Per-Task Cost โœ… Captured locally (this sprint)
T2.x Cost-Aware Routing ๐Ÿ“‹ Hive matrix โ†’ routing decision
T2.x Model Selection ๐Ÿ“‹ Arbitrage between cloud tiers + local LLM
T2.x Eval Harness ๐Ÿ“‹ Task success measurement

The key insight: local LLM is the ultimate cache-hit token. Every token generated on a beefy nodeโ€™s GPU is $0.0000. The routing engineโ€™s job is to maximize the use of $0 tokens without compromising task success rates.

Two one-liner configs that prevent SSH interruptions and ksshaskpass popups on fleet nodes:

When a fleet node connects and the key doesnโ€™t match on first attempt, sshd falls back to password authentication. Fail2ban counts those as failures and bans the source IP after maxretry attempts. With password auth off, there is nothing to brute-force:

/etc/ssh/sshd_config
PasswordAuthentication no

Caveat: nodes with password auth disabled need physical/console access if they lose their private key.

When ssh-agent has no identities, Kitty SSH triggers ksshaskpass on reconnect. AddKeysToAgent yes auto-loads keys on first use:

~/.ssh/config
Host *
AddKeysToAgent yes

On FreeBSD nodes, max-src-conn-rate 5/60 with <ssh_brutes> table provides the same protection independently of fail2ban:

/etc/pf.conf
table <ssh_brutes> persist
pass in proto tcp to port 22 \
max-src-conn-rate 5/60 overload <ssh_brutes> flush global

โ†’ fail2ban-tailscale skill โ†’ freebsd-admin skill