From d3d52349c3431cb03c0b976fa0e5ec6cfc8aa34d Mon Sep 17 00:00:00 2001 From: damocles Date: Wed, 20 May 2026 15:42:56 +0200 Subject: [PATCH] model/context: move context window config to host-level hive-c0re.nix --- hive-c0re/src/actions.rs | 2 ++ hive-c0re/src/auto_update.rs | 2 ++ hive-c0re/src/coordinator.rs | 9 +++++ hive-c0re/src/lifecycle.rs | 6 ++-- hive-c0re/src/main.rs | 11 ++++++ hive-c0re/src/meta.rs | 19 +++++++++-- hive-c0re/src/migrate.rs | 1 + hive-c0re/src/server.rs | 2 ++ nix/modules/hive-c0re.nix | 27 ++++++++++++++- nix/templates/harness-base.nix | 61 +++++----------------------------- 10 files changed, 81 insertions(+), 59 deletions(-) diff --git a/hive-c0re/src/actions.rs b/hive-c0re/src/actions.rs index 3d8e224..0fa0560 100644 --- a/hive-c0re/src/actions.rs +++ b/hive-c0re/src/actions.rs @@ -91,6 +91,7 @@ pub async fn approve(coord: Arc, id: i64) -> Result<()> { ¬es_dir, coord_bg.dashboard_port, &coord_bg.operator_pronouns, + &coord_bg.context_window_tokens, ) .await; drop(guard); @@ -415,6 +416,7 @@ async fn sync_meta_after_lifecycle(coord: &Coordinator) -> Result<()> { &coord.hyperhive_flake, coord.dashboard_port, &coord.operator_pronouns, + &coord.context_window_tokens, &agents, ) .await diff --git a/hive-c0re/src/auto_update.rs b/hive-c0re/src/auto_update.rs index 9a18bd7..3acc674 100644 --- a/hive-c0re/src/auto_update.rs +++ b/hive-c0re/src/auto_update.rs @@ -73,6 +73,7 @@ pub async fn rebuild_agent(coord: &Arc, name: &str, current_rev: &s ¬es_dir, coord.dashboard_port, &coord.operator_pronouns, + &coord.context_window_tokens, ) .await; drop(guard); @@ -160,6 +161,7 @@ pub async fn ensure_manager(coord: &Arc) -> Result<()> { ¬es_dir, coord.dashboard_port, &coord.operator_pronouns, + &coord.context_window_tokens, ) .await?; if let Some(rev) = current_rev { diff --git a/hive-c0re/src/coordinator.rs b/hive-c0re/src/coordinator.rs index a291443..3a85bf2 100644 --- a/hive-c0re/src/coordinator.rs +++ b/hive-c0re/src/coordinator.rs @@ -51,6 +51,13 @@ pub struct Coordinator { /// meta flake); the harness substitutes it into the agent / /// manager system prompt at boot. pub operator_pronouns: String, + /// Per-model context-window sizes in tokens. Set via the host-level + /// `services.hive-c0re.contextWindowTokens` NixOS option; injected + /// into each container as `HIVE_CONTEXT_WINDOW_TOKENS_` + /// by the meta flake renderer. The harness uses these to derive + /// compaction / auto-reset watermarks and exposes the active value + /// on `/api/state` as `context_window_tokens`. + pub context_window_tokens: std::collections::HashMap, agents: Mutex>, /// Agents whose lifecycle action (currently just spawn) is in flight. /// Read by the dashboard to render a spinner; cleared when the action @@ -139,6 +146,7 @@ impl Coordinator { hyperhive_flake: String, dashboard_port: u16, operator_pronouns: String, + context_window_tokens: std::collections::HashMap, ) -> Result { let broker = Broker::open(db_path).context("open broker")?; let approvals = Approvals::open(db_path).context("open approvals")?; @@ -152,6 +160,7 @@ impl Coordinator { hyperhive_flake, dashboard_port, operator_pronouns, + context_window_tokens, agents: Mutex::new(HashMap::new()), transient: Mutex::new(HashMap::new()), dashboard_events, diff --git a/hive-c0re/src/lifecycle.rs b/hive-c0re/src/lifecycle.rs index 1e07352..cd29476 100644 --- a/hive-c0re/src/lifecycle.rs +++ b/hive-c0re/src/lifecycle.rs @@ -138,6 +138,7 @@ pub async fn spawn( notes_dir: &Path, dashboard_port: u16, operator_pronouns: &str, + context_window_tokens: &std::collections::HashMap, ) -> Result<()> { validate(name)?; if let Some(other) = port_collision(name).await { @@ -154,7 +155,7 @@ pub async fn spawn( // before `nixos-container create` so the `--flake meta#` // ref resolves. let agents = agents_after_spawn(name).await?; - crate::meta::sync_agents(hyperhive_flake, dashboard_port, operator_pronouns, &agents).await?; + crate::meta::sync_agents(hyperhive_flake, dashboard_port, operator_pronouns, context_window_tokens, &agents).await?; let container = container_name(name); let flake_ref = format!("{}#{name}", crate::meta::meta_dir().display()); run(&["create", &container, "--flake", &flake_ref]).await?; @@ -273,6 +274,7 @@ pub async fn rebuild( notes_dir: &Path, dashboard_port: u16, operator_pronouns: &str, + context_window_tokens: &std::collections::HashMap, ) -> Result<()> { // Sync the meta flake (idempotent — no-op when the rendered // flake matches disk) so a manual rebuild from the dashboard @@ -280,7 +282,7 @@ pub async fn rebuild( // got added directly via `nixos-container create` outside // hive-c0re). let agents = agents_for_meta(None).await?; - crate::meta::sync_agents(hyperhive_flake, dashboard_port, operator_pronouns, &agents).await?; + crate::meta::sync_agents(hyperhive_flake, dashboard_port, operator_pronouns, context_window_tokens, &agents).await?; // Then bump just this agent's input — picks up whatever // `applied//main` currently points at (deployed/). // Commits the lock if it changed. diff --git a/hive-c0re/src/main.rs b/hive-c0re/src/main.rs index 1206567..4948a95 100644 --- a/hive-c0re/src/main.rs +++ b/hive-c0re/src/main.rs @@ -62,6 +62,12 @@ enum Cmd { /// system prompt can mention them. Default: `she/her`. #[arg(long, default_value = "she/her")] operator_pronouns: String, + /// Per-model context-window sizes, as JSON object mapping model-family + /// short name to token count. Threaded into each container as + /// `HIVE_CONTEXT_WINDOW_TOKENS_` env vars. Set via the + /// `services.hive-c0re.contextWindowTokens` NixOS option. + #[arg(long, default_value = r#"{"haiku":200000,"sonnet":1000000,"opus":1000000}"#)] + context_window_tokens: String, }, /// Spawn a new agent container directly (`hive-agent-`). Bypasses /// the approval queue — use only as an operator on the host. For @@ -109,12 +115,17 @@ async fn main() -> Result<()> { db, dashboard_port, operator_pronouns, + context_window_tokens, } => { + let cwt: std::collections::HashMap = + serde_json::from_str(&context_window_tokens) + .context("--context-window-tokens: invalid JSON")?; let coord = Arc::new(Coordinator::open( &db, hyperhive_flake, dashboard_port, operator_pronouns, + cwt, )?); manager_server::start(coord.clone())?; // Idempotent pre-flight: rewrite pre-meta-layout applied diff --git a/hive-c0re/src/meta.rs b/hive-c0re/src/meta.rs index 48f4fde..55b01dd 100644 --- a/hive-c0re/src/meta.rs +++ b/hive-c0re/src/meta.rs @@ -66,13 +66,14 @@ pub async fn sync_agents( hyperhive_flake: &str, dashboard_port: u16, operator_pronouns: &str, + context_window_tokens: &std::collections::HashMap, agents: &[AgentSpec], ) -> Result<()> { let _guard = META_LOCK.lock().await; let dir = meta_dir(); std::fs::create_dir_all(&dir).with_context(|| format!("create {}", dir.display()))?; - let new_flake = render_flake(hyperhive_flake, dashboard_port, operator_pronouns, agents); + let new_flake = render_flake(hyperhive_flake, dashboard_port, operator_pronouns, context_window_tokens, agents); let flake_path = dir.join("flake.nix"); let on_disk = std::fs::read_to_string(&flake_path).unwrap_or_default(); let initial = !dir.join(".git").exists(); @@ -235,6 +236,7 @@ fn render_flake( hyperhive_flake: &str, dashboard_port: u16, operator_pronouns: &str, + context_window_tokens: &std::collections::HashMap, agents: &[AgentSpec], ) -> String { use std::fmt::Write as _; @@ -283,8 +285,19 @@ fn render_flake( HIVE_PORT = toString port; HIVE_LABEL = name; HIVE_DASHBOARD_PORT = toString dashboardPort; - HIVE_OPERATOR_PRONOUNS = operatorPronouns; - HYPERHIVE_STATE_DIR = "/agents/${name}/state"; + HIVE_OPERATOR_PRONOUNS = operatorPronouns;"#, + ); + // Per-model context-window env vars declared in the host-level + // `services.hive-c0re.contextWindowTokens` option. Use a sorted + // iterator for deterministic flake output (no spurious git diffs). + let mut sorted_tokens: Vec<(&String, &u64)> = context_window_tokens.iter().collect(); + sorted_tokens.sort_by_key(|(k, _)| k.as_str()); + for (key, val) in &sorted_tokens { + let upper_key = key.to_ascii_uppercase(); + let _ = writeln!(out, " HIVE_CONTEXT_WINDOW_TOKENS_{upper_key} = \"{val}\";"); + } + out.push_str( + r#" HYPERHIVE_STATE_DIR = "/agents/${name}/state"; }; } ]; diff --git a/hive-c0re/src/migrate.rs b/hive-c0re/src/migrate.rs index 43fe95d..5e1ae42 100644 --- a/hive-c0re/src/migrate.rs +++ b/hive-c0re/src/migrate.rs @@ -83,6 +83,7 @@ pub async fn run(coord: &Arc) -> Result<()> { &coord.hyperhive_flake, coord.dashboard_port, &coord.operator_pronouns, + &coord.context_window_tokens, &agents, ) .await diff --git a/hive-c0re/src/server.rs b/hive-c0re/src/server.rs index 639475e..42ccc5f 100644 --- a/hive-c0re/src/server.rs +++ b/hive-c0re/src/server.rs @@ -77,6 +77,7 @@ async fn dispatch(req: &HostRequest, coord: Arc) -> HostResponse { ¬es_dir, coord.dashboard_port, &coord.operator_pronouns, + &coord.context_window_tokens, ) .await { @@ -139,6 +140,7 @@ async fn dispatch(req: &HostRequest, coord: Arc) -> HostResponse { ¬es_dir, coord.dashboard_port, &coord.operator_pronouns, + &coord.context_window_tokens, ) .await; // Mirror auto_update::rebuild_agent — the manager wants diff --git a/nix/modules/hive-c0re.nix b/nix/modules/hive-c0re.nix index 158eefe..9325741 100644 --- a/nix/modules/hive-c0re.nix +++ b/nix/modules/hive-c0re.nix @@ -57,6 +57,31 @@ in approval needed. ''; }; + contextWindowTokens = lib.mkOption { + type = lib.types.attrsOf lib.types.int; + default = { + haiku = 200000; + sonnet = 1000000; + opus = 1000000; + }; + example = { + haiku = 150000; + sonnet = 900000; + }; + description = '' + Per-model context-window sizes in tokens. Each key is a + model-family short name matched case-insensitively as a + substring of the active model name at runtime (e.g. `"sonnet"` + matches `"claude-sonnet-4-5"`). The defaults cover the known + Anthropic families; add entries for new models or override + existing ones here to change the window for all agents at once. + + Passed to `hive-c0re serve` as JSON and injected into every + container's harness service environment as + `HIVE_CONTEXT_WINDOW_TOKENS_`. Changes propagate + on the next `↻ R3BU1LD` — no per-agent approval needed. + ''; + }; }; config = lib.mkIf cfg.enable { @@ -89,7 +114,7 @@ in ]; environment.HYPERHIVE_GIT = "${pkgs.git}/bin/git"; serviceConfig = { - ExecStart = "${cfg.package}/bin/hive-c0re --socket /run/hyperhive/host.sock serve --hyperhive-flake ${cfg.hyperhiveFlake} --dashboard-port ${toString cfg.dashboardPort} --operator-pronouns ${lib.escapeShellArg cfg.operatorPronouns}"; + ExecStart = "${cfg.package}/bin/hive-c0re --socket /run/hyperhive/host.sock serve --hyperhive-flake ${cfg.hyperhiveFlake} --dashboard-port ${toString cfg.dashboardPort} --operator-pronouns ${lib.escapeShellArg cfg.operatorPronouns} --context-window-tokens ${lib.escapeShellArg (builtins.toJSON cfg.contextWindowTokens)}"; Restart = "on-failure"; RestartSec = 2; RuntimeDirectory = "hyperhive"; diff --git a/nix/templates/harness-base.nix b/nix/templates/harness-base.nix index d3b4956..25bfa20 100644 --- a/nix/templates/harness-base.nix +++ b/nix/templates/harness-base.nix @@ -36,41 +36,6 @@ ''; }; - options.hyperhive.contextWindowTokens = lib.mkOption { - type = lib.types.attrsOf lib.types.int; - # Canonical defaults for known Anthropic model families. - # Override any entry in your agent.nix, or add new keys for - # model families not listed here. - default = { - haiku = 200000; - sonnet = 1000000; - opus = 1000000; - }; - example = { - haiku = 150000; - sonnet = 900000; - }; - description = '' - Per-model context-window sizes in tokens. Each key is a - model-family short name (e.g. `"haiku"`, `"sonnet"`) matched as a - case-insensitive substring of the active model name at runtime, so - `"sonnet"` matches `"claude-sonnet-4-5"` and any future variant. - - The defaults declared here cover the known Anthropic model families. - Add or override entries in your `agent.nix` when using a - non-standard model or when Anthropic changes a model's window. - - Each entry is rendered as - `HIVE_CONTEXT_WINDOW_TOKENS_` (e.g. - `HIVE_CONTEXT_WINDOW_TOKENS_SONNET = "1000000"`). The harness - checks these per-model vars in order (first substring match wins), - then falls back to `200000` when no key matches. At runtime the - effective window drives compaction (75%) and auto-reset (50%) - watermarks, and is exposed via `/api/state` as - `context_window_tokens`. - ''; - }; - options.hyperhive.allowedBashPatterns = lib.mkOption { type = lib.types.listOf lib.types.str; default = [ ]; @@ -264,25 +229,15 @@ environment.etc."hyperhive/claude-plugins-auto-update.json".text = builtins.toJSON config.hyperhive.claudePluginsAutoUpdate; - # Model + context-window env vars consumed by the harness at boot. # HIVE_DEFAULT_MODEL seeds the initial model selection when no persisted - # model choice exists in the state dir. - # HIVE_CONTEXT_WINDOW_TOKENS_ provides per-model overrides - # (e.g. HIVE_CONTEXT_WINDOW_TOKENS_SONNET) from contextWindowTokens attrset. - # SHELL must be set so claude's Bash tool finds a POSIX shell. - environment.variables = lib.mkMerge ( - [ - { - HIVE_DEFAULT_MODEL = config.hyperhive.model; - SHELL = "${pkgs.bashInteractive}/bin/bash"; - } - ] - ++ lib.mapAttrsToList - (model: tokens: { - "HIVE_CONTEXT_WINDOW_TOKENS_${lib.toUpper model}" = toString tokens; - }) - config.hyperhive.contextWindowTokens - ); + # model choice exists in the state dir. SHELL must be set so claude's + # Bash tool finds a POSIX shell. + # HIVE_CONTEXT_WINDOW_TOKENS_* are injected by the meta flake from the + # host-level `services.hive-c0re.contextWindowTokens` option — not set here. + environment.variables = { + HIVE_DEFAULT_MODEL = config.hyperhive.model; + SHELL = "${pkgs.bashInteractive}/bin/bash"; + }; boot.isNspawnContainer = true;