0.12.0: hw-probe autospawn + model fixes + mother schema
Some checks failed
CI / rust (pull_request) Has been cancelled
CI / markdown (pull_request) Has been cancelled
CI / port (pull_request) Has been cancelled
CI / agent-jail-pkgs (pull_request) Has been cancelled

Combined:
- daemon runs clawdie-hw-probe before autospawn, passes CLAWDIE_HW_PROFILE
- DEFAULT_MODEL → deepseek-v4-pro, version → 0.12.0
- PostgreSQL schema: usb_nodes, build_queue, audit_log + capability trigger
This commit is contained in:
Sam & Claude 2026-06-23 10:50:05 +02:00
parent 1a49485dbd
commit 381f530ed8
4 changed files with 132 additions and 4 deletions

View file

@ -2,7 +2,7 @@
members = ["crates/colibri-contracts", "crates/colibri-deepseek", "crates/colibri-runtime", "crates/colibri-glasspane", "crates/colibri-daemon", "crates/colibri-client", "crates/colibri-glasspane-tui", "crates/colibri-store", "crates/colibri-skills", "crates/colibri-mcp", "crates/colibri-vault", "crates/clawdie"]
[workspace.package]
version = "0.11.0"
version = "0.12.0"
[package]
name = "colibri"

View file

@ -243,6 +243,7 @@ async fn dispatch(cmd: ColibriCommand, state: &SharedState) -> ColibriResponse {
system_prompt,
local_args,
jail,
HashMap::new(),
)
.await
}
@ -449,6 +450,44 @@ pub async fn autospawn_pi_if_configured(state: &SharedState) {
info!(binary = %pi_binary, ?args, "autospawn-pi: spawning Pi agent on host (DeepSeek-backed)");
// Collect hardware profile via clawdie-hw-probe (non-blocking).
// Pass it to the spawned agent as CLAWDIE_HW_PROFILE so the agent can
// self-describe the host hardware without running probes itself.
let mut extra_env: HashMap<String, String> = HashMap::new();
let probe_binary = "/usr/local/bin/clawdie-hw-probe";
if std::path::Path::new(probe_binary).exists() {
match std::process::Command::new(probe_binary).output() {
Ok(output) if output.status.success() => {
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
if !stdout.is_empty() {
info!(
probe_bytes = output.stdout.len(),
"autospawn-pi: collected hardware profile"
);
extra_env.insert("CLAWDIE_HW_PROFILE".to_string(), stdout);
} else {
warn!("autospawn-pi: clawdie-hw-probe returned empty stdout");
}
}
Ok(output) => {
let stderr = String::from_utf8_lossy(&output.stderr);
warn!(
status = %output.status,
stderr = %stderr.trim(),
"autospawn-pi: clawdie-hw-probe failed (continuing without hw profile)"
);
}
Err(e) => {
warn!(
error = %e,
"autospawn-pi: failed to run clawdie-hw-probe (continuing without hw profile)"
);
}
}
} else {
debug!("autospawn-pi: clawdie-hw-probe not found at {probe_binary}; skipping hw profile");
}
// provider=local → binary is the Pi executable; jail=None → host-spawn.
let resp = cmd_spawn_agent(
state,
@ -458,6 +497,7 @@ pub async fn autospawn_pi_if_configured(state: &SharedState) {
None,
Some(args),
None,
extra_env,
)
.await;
@ -498,6 +538,7 @@ async fn cmd_spawn_agent(
system_prompt: Option<String>,
local_args: Option<Vec<String>>,
jail: Option<JailConfig>,
extra_env: HashMap<String, String>,
) -> ColibriResponse {
let provider = match provider_str.to_lowercase().as_str() {
"deepseek" => Provider::DeepSeek,
@ -531,8 +572,10 @@ async fn cmd_spawn_agent(
..Default::default()
};
// T1.4 PR3a: inject session prompt context as env var when enabled
let mut extra_env = HashMap::new();
// T1.4 PR3a: inject session prompt context as env var when enabled.
// Start with any caller-provided extra env (e.g. CLAWDIE_HW_PROFILE from
// autospawn), then layer in scheduler prompt context on top.
let mut extra_env = extra_env;
if state.config.scheduler_prompt_injection {
if let Some(ref sid) = session_id {
if let Some(session) = state.sessions.get(sid) {

View file

@ -22,7 +22,7 @@ use serde_json::{json, Value};
pub const DEFAULT_ENDPOINT: &str = "https://api.deepseek.com/chat/completions";
// DeepSeek API model string (cache-capable). Distinct from our internal
// `deepseek-v4-flash` alias; override with DEEPSEEK_MODEL.
pub const DEFAULT_MODEL: &str = "deepseek-chat";
pub const DEFAULT_MODEL: &str = "deepseek-v4-pro";
// Deliberately long and byte-stable. Reasonix discipline: the immutable region
// must not change between turns or the cache will not hit.

View file

@ -0,0 +1,85 @@
CREATE TABLE IF NOT EXISTS usb_nodes (
id SERIAL PRIMARY KEY,
hostname TEXT NOT NULL UNIQUE,
last_seen TIMESTAMPTZ NOT NULL DEFAULT now(),
first_seen TIMESTAMPTZ NOT NULL DEFAULT now(),
freebsd_version JSONB,
hw_profile JSONB NOT NULL,
capabilities JSONB,
status TEXT NOT NULL DEFAULT 'offline',
tags TEXT[] DEFAULT '{}',
last_cap_sync TIMESTAMPTZ
);
CREATE INDEX IF NOT EXISTS idx_nodes_status ON usb_nodes (status);
CREATE INDEX IF NOT EXISTS idx_nodes_last_seen ON usb_nodes (last_seen DESC);
CREATE INDEX IF NOT EXISTS idx_nodes_cap_has_gpu ON usb_nodes ((capabilities->>'has_gpu'));
CREATE TABLE IF NOT EXISTS build_queue (
id SERIAL PRIMARY KEY,
node_id INTEGER REFERENCES usb_nodes(id),
crate TEXT NOT NULL DEFAULT 'colibri-daemon',
branch TEXT NOT NULL DEFAULT 'main',
release BOOLEAN NOT NULL DEFAULT true,
status TEXT NOT NULL DEFAULT 'queued',
priority INTEGER NOT NULL DEFAULT 0,
queued_at TIMESTAMPTZ NOT NULL DEFAULT now(),
started_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
binary_path TEXT,
binary_size BIGINT,
commit_sha TEXT,
duration_s INTEGER,
error_log TEXT
);
CREATE INDEX IF NOT EXISTS idx_build_status ON build_queue (status, priority DESC, queued_at);
CREATE TABLE IF NOT EXISTS audit_log (
id BIGSERIAL PRIMARY KEY,
event_ts TIMESTAMPTZ NOT NULL DEFAULT now(),
event_type TEXT NOT NULL,
node_id INTEGER REFERENCES usb_nodes(id),
build_id INTEGER REFERENCES build_queue(id),
details JSONB
);
CREATE INDEX IF NOT EXISTS idx_audit_ts ON audit_log (event_ts DESC);
CREATE OR REPLACE FUNCTION derive_capabilities()
RETURNS trigger AS $$
DECLARE
ram INTEGER;
drivers TEXT;
wifi JSONB;
caps JSONB := '{}'::JSONB;
BEGIN
ram := COALESCE((NEW.hw_profile->>'ram_gb')::INTEGER, 0);
drivers := NEW.hw_profile->>'gpu_driver';
wifi := NEW.hw_profile->'wifi';
IF NEW.hw_profile->'gpu' IS NOT NULL AND jsonb_array_length(NEW.hw_profile->'gpu') > 0 THEN
caps := caps || '{"has_gpu": true}'::JSONB;
IF drivers ILIKE '%amdgpu%' THEN
caps := caps || '{"gpu_vendor": "amd", "vulkan_compute": true}'::JSONB;
ELSIF drivers ILIKE '%nvidia%' THEN
caps := caps || '{"gpu_vendor": "nvidia"}'::JSONB;
END IF;
ELSE
caps := caps || '{"has_gpu": false, "cpu_only": true}'::JSONB;
END IF;
IF caps->>'has_gpu' = 'true' AND caps->>'gpu_vendor' = 'nvidia' THEN
caps := caps || '{"can_run_local_llm": true}'::JSONB;
IF ram >= 64 THEN caps := caps || '{"max_model": "13b-q4"}'::JSONB;
ELSIF ram >= 32 THEN caps := caps || '{"max_model": "7b-q4"}'::JSONB;
END IF;
ELSIF ram >= 16 THEN
caps := caps || '{"can_run_local_llm": true, "max_model": "3b"}'::JSONB;
END IF;
IF wifi IS NOT NULL AND jsonb_array_length(wifi) > 0 THEN
caps := caps || '{"has_wifi": true}'::JSONB;
END IF;
NEW.capabilities := caps;
NEW.last_cap_sync := now();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS trg_derive_capabilities ON usb_nodes;
CREATE TRIGGER trg_derive_capabilities
BEFORE INSERT OR UPDATE OF hw_profile ON usb_nodes
FOR EACH ROW EXECUTE FUNCTION derive_capabilities();