From 381f530ed85d6bc17b71ab37cda43b9a187e3344 Mon Sep 17 00:00:00 2001 From: Sam & Claude Date: Tue, 23 Jun 2026 10:50:05 +0200 Subject: [PATCH] 0.12.0: hw-probe autospawn + model fixes + mother schema MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- Cargo.toml | 2 +- crates/colibri-daemon/src/socket.rs | 47 +++++++++++++++- crates/colibri-deepseek/src/lib.rs | 2 +- packaging/mother/mother_schema.sql | 85 +++++++++++++++++++++++++++++ 4 files changed, 132 insertions(+), 4 deletions(-) create mode 100644 packaging/mother/mother_schema.sql diff --git a/Cargo.toml b/Cargo.toml index 7138299..d6855af 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/crates/colibri-daemon/src/socket.rs b/crates/colibri-daemon/src/socket.rs index 198e84c..a62210d 100644 --- a/crates/colibri-daemon/src/socket.rs +++ b/crates/colibri-daemon/src/socket.rs @@ -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 = 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, local_args: Option>, jail: Option, + extra_env: HashMap, ) -> 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) { diff --git a/crates/colibri-deepseek/src/lib.rs b/crates/colibri-deepseek/src/lib.rs index 50bde14..8646590 100644 --- a/crates/colibri-deepseek/src/lib.rs +++ b/crates/colibri-deepseek/src/lib.rs @@ -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. diff --git a/packaging/mother/mother_schema.sql b/packaging/mother/mother_schema.sql new file mode 100644 index 0000000..7be17ca --- /dev/null +++ b/packaging/mother/mother_schema.sql @@ -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();