From c5847e25eadce55a46159b13bda3f57dd108ff9d Mon Sep 17 00:00:00 2001 From: damocles Date: Sun, 28 Jun 2026 23:25:58 +0200 Subject: [PATCH] feat(#2067): choom takes optional session id to resume, else blank session --- docs/tools/hivectl-cli.md | 5 ++-- docs/tools/hivectl.md | 13 +++++---- hive-c0re/src/bin/hivectl.rs | 56 +++++++++++++++++++++++++----------- 3 files changed, 51 insertions(+), 23 deletions(-) diff --git a/docs/tools/hivectl-cli.md b/docs/tools/hivectl-cli.md index 809fc9aa..403b7041 100644 --- a/docs/tools/hivectl-cli.md +++ b/docs/tools/hivectl-cli.md @@ -379,13 +379,14 @@ Generate the federation peer-config block for THIS hive — the nix a peer opera Open an interactive Claude session inside an agent container. -Execs `machinectl shell @h-` running claude as the agent user from its state dir, with the same settings / MCP config / system prompt the harness uses. Each launch starts a fresh, named ("choom") session so it never collides with the harness's live session in the same project dir. Requires root + a running container. +Execs `machinectl shell @h-` running claude as the agent user from its state dir, with the same settings / MCP config / system prompt the harness uses. Without a session id you get a fresh blank session each launch; pass one to resume an earlier choom session (`--resume `). Either way it never collides with the harness's live session — the harness pins its own session id via `--resume`, so a blank choom session is invisible to it. Requires root + a running container. -**Usage:** `hivectl choom ` +**Usage:** `hivectl choom [SESSION]` ###### **Arguments:** * `` — Agent name (e.g. `damocles`, `iris`) +* `` — Optional session id to resume (`claude --resume `). Omit for a fresh blank session diff --git a/docs/tools/hivectl.md b/docs/tools/hivectl.md index 9dbed3b5..714eb0ab 100644 --- a/docs/tools/hivectl.md +++ b/docs/tools/hivectl.md @@ -127,13 +127,16 @@ running claude from the agent's state dir. Requires root (same as all `machinectl shell` operations). ```bash -hivectl choom iris # fresh Claude session named "choom" in iris's container +hivectl choom iris # fresh blank Claude session in iris's container +hivectl choom iris # resume an earlier choom session ``` -Each launch starts a fresh session named `choom` (claude mints its own -id), so it never collides with the harness's live session in the same -project dir. The operator can rejoin a prior choom session later with -`claude --resume choom`. The container must be running. +With no session id, claude starts a fresh blank session (mints its own +id). Pass a session id to resume it (`claude --resume `); you can +rename it in-session. Either way choom never collides with the harness's +live session in the same project dir: the harness pins its own id via +`--resume`, so a blank choom session is invisible to it. The container +must be running. choom reproduces the harness's own claude invocation so the operator lands in a faithful copy of the agent's environment: diff --git a/hive-c0re/src/bin/hivectl.rs b/hive-c0re/src/bin/hivectl.rs index be5399eb..e348d5a7 100644 --- a/hive-c0re/src/bin/hivectl.rs +++ b/hive-c0re/src/bin/hivectl.rs @@ -112,13 +112,18 @@ enum Cmd { /// /// Execs `machinectl shell @h-` running claude as the /// agent user from its state dir, with the same settings / MCP config - /// / system prompt the harness uses. Each launch starts a fresh, - /// named ("choom") session so it never collides with the harness's - /// live session in the same project dir. Requires root + a running - /// container. + /// / system prompt the harness uses. Without a session id you get a + /// fresh blank session each launch; pass one to resume an earlier + /// choom session (`--resume `). Either way it never collides with + /// the harness's live session — the harness pins its own session id + /// via `--resume`, so a blank choom session is invisible to it. + /// Requires root + a running container. Choom { /// Agent name (e.g. `damocles`, `iris`). name: String, + /// Optional session id to resume (`claude --resume `). Omit + /// for a fresh blank session. + session: Option, }, /// Stop containers hive-wide in one operator action. Bare `hivectl /// stop` stops **everything** — all sub-agents plus the ci, forge, @@ -625,7 +630,7 @@ async fn main() -> Result<()> { Cmd::Subvol { cmd } => match cmd { SubvolCmd::Upgrade { name, yes } => subvol_upgrade(&socket, &name, yes).await, }, - Cmd::Choom { name } => choom(&name), + Cmd::Choom { name, session } => choom(&name, session.as_deref()), Cmd::Quota { cmd } => match cmd { QuotaCmd::Enable => quota_enable().await, QuotaCmd::Show { name } => quota_show(name.as_deref()).await, @@ -1029,14 +1034,15 @@ fn agent_exists(name: &str) -> Result { /// 3. **Pass the harness's flags** (`--settings` / `--mcp-config` / /// `--system-prompt-file` from `/run/hive-config/`), each only if its /// file exists so a half-up container degrades to a bare session. -/// 4. **Use a fresh, named session.** `--name choom` starts a brand-new -/// session (claude mints a random id) tagged "choom", so it never -/// resumes the harness's live session in this shared project dir. The -/// operator can rejoin a prior choom session later via `claude -/// --resume choom`. +/// 4. **Pick the session explicitly.** With no `session` arg, claude +/// starts a fresh blank session (mints a random id). Pass a session +/// id to resume it (`--resume `); the operator can rename it +/// in-session. Either way choom never clobbers the harness's live +/// session: the harness pins its own id via `--resume`, so a blank +/// choom session in this shared project dir is invisible to it. /// /// Inherits the caller's PTY. Requires root + a running container. -fn choom(name: &str) -> Result<()> { +fn choom(name: &str, session: Option<&str>) -> Result<()> { if !agent_exists(name)? { bail!("no such agent: '{name}' (no state dir under /var/lib/hyperhive/agents/)"); } @@ -1053,15 +1059,33 @@ fn choom(name: &str) -> Result<()> { // `hive-config`); matches `hive-ag3nt::paths::config_dir()`. These // are the exact files the harness passes to claude each turn. let cfg = "/run/hive-config"; + // A session id (if given) is resumed via `--resume `; with none, + // claude starts a fresh blank session. The id is interpolated into + // the shell script, so constrain it to a safe charset first to rule + // out injection (real session ids are UUIDs). + let session_arg = match session { + Some(sid) => { + if sid.is_empty() + || !sid + .bytes() + .all(|b| b.is_ascii_alphanumeric() || b"-_.".contains(&b)) + { + bail!( + "invalid session id '{sid}': expected a claude session id (alphanumerics, '-', '_', '.')" + ); + } + format!("set -- --resume {sid};") + } + None => "set --;".to_string(), + }; // Build the claude argv as the harness does, but only include each // flag when its file is present so a half-up container falls back to // a bare session rather than a hard claude error. `name` is - // constrained to `[a-z0-9._-]` so there's nothing to quote. - // `--name choom` starts a fresh, named session each launch (claude - // mints its own id), so choom never collides with the harness's live - // session in this same project dir; `--resume choom` rejoins it later. + // constrained to `[a-z0-9._-]` so there's nothing to quote. choom + // never collides with the harness's live session in this same project + // dir: the harness pins its own session id via `--resume`. let inner = format!( - "cd {state_dir} || exit 1; set -- --name choom; \ + "cd {state_dir} || exit 1; {session_arg} \ [ -f {cfg}/claude-settings.json ] && set -- \"$@\" --settings {cfg}/claude-settings.json; \ [ -f {cfg}/claude-mcp-config.json ] && set -- \"$@\" --mcp-config {cfg}/claude-mcp-config.json; \ [ -f {cfg}/claude-system-prompt.md ] && set -- \"$@\" --system-prompt-file {cfg}/claude-system-prompt.md; \