feat(#2067): choom takes optional session id to resume, else blank session

This commit is contained in:
damocles 2026-06-28 23:25:58 +02:00 committed by mara
commit c5847e25ea
3 changed files with 51 additions and 23 deletions

View file

@ -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. Open an interactive Claude session inside an agent container.
Execs `machinectl shell <name>@h-<name>` 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 <name>@h-<name>` 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 <id>`). 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 <NAME>` **Usage:** `hivectl choom <NAME> [SESSION]`
###### **Arguments:** ###### **Arguments:**
* `<NAME>` — Agent name (e.g. `damocles`, `iris`) * `<NAME>` — Agent name (e.g. `damocles`, `iris`)
* `<SESSION>` — Optional session id to resume (`claude --resume <id>`). Omit for a fresh blank session

View file

@ -127,13 +127,16 @@ running claude from the agent's state dir. Requires root (same as all
`machinectl shell` operations). `machinectl shell` operations).
```bash ```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 <session-id> # resume an earlier choom session
``` ```
Each launch starts a fresh session named `choom` (claude mints its own With no session id, claude starts a fresh blank session (mints its own
id), so it never collides with the harness's live session in the same id). Pass a session id to resume it (`claude --resume <id>`); you can
project dir. The operator can rejoin a prior choom session later with rename it in-session. Either way choom never collides with the harness's
`claude --resume choom`. The container must be running. 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 choom reproduces the harness's own claude invocation so the operator
lands in a faithful copy of the agent's environment: lands in a faithful copy of the agent's environment:

View file

@ -112,13 +112,18 @@ enum Cmd {
/// ///
/// Execs `machinectl shell <name>@h-<name>` running claude as the /// Execs `machinectl shell <name>@h-<name>` running claude as the
/// agent user from its state dir, with the same settings / MCP config /// agent user from its state dir, with the same settings / MCP config
/// / system prompt the harness uses. Each launch starts a fresh, /// / system prompt the harness uses. Without a session id you get a
/// named ("choom") session so it never collides with the harness's /// fresh blank session each launch; pass one to resume an earlier
/// live session in the same project dir. Requires root + a running /// choom session (`--resume <id>`). Either way it never collides with
/// container. /// 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 { Choom {
/// Agent name (e.g. `damocles`, `iris`). /// Agent name (e.g. `damocles`, `iris`).
name: String, name: String,
/// Optional session id to resume (`claude --resume <id>`). Omit
/// for a fresh blank session.
session: Option<String>,
}, },
/// Stop containers hive-wide in one operator action. Bare `hivectl /// Stop containers hive-wide in one operator action. Bare `hivectl
/// stop` stops **everything** — all sub-agents plus the ci, forge, /// stop` stops **everything** — all sub-agents plus the ci, forge,
@ -625,7 +630,7 @@ async fn main() -> Result<()> {
Cmd::Subvol { cmd } => match cmd { Cmd::Subvol { cmd } => match cmd {
SubvolCmd::Upgrade { name, yes } => subvol_upgrade(&socket, &name, yes).await, 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 { Cmd::Quota { cmd } => match cmd {
QuotaCmd::Enable => quota_enable().await, QuotaCmd::Enable => quota_enable().await,
QuotaCmd::Show { name } => quota_show(name.as_deref()).await, QuotaCmd::Show { name } => quota_show(name.as_deref()).await,
@ -1029,14 +1034,15 @@ fn agent_exists(name: &str) -> Result<bool> {
/// 3. **Pass the harness's flags** (`--settings` / `--mcp-config` / /// 3. **Pass the harness's flags** (`--settings` / `--mcp-config` /
/// `--system-prompt-file` from `/run/hive-config/`), each only if its /// `--system-prompt-file` from `/run/hive-config/`), each only if its
/// file exists so a half-up container degrades to a bare session. /// file exists so a half-up container degrades to a bare session.
/// 4. **Use a fresh, named session.** `--name choom` starts a brand-new /// 4. **Pick the session explicitly.** With no `session` arg, claude
/// session (claude mints a random id) tagged "choom", so it never /// starts a fresh blank session (mints a random id). Pass a session
/// resumes the harness's live session in this shared project dir. The /// id to resume it (`--resume <id>`); the operator can rename it
/// operator can rejoin a prior choom session later via `claude /// in-session. Either way choom never clobbers the harness's live
/// --resume choom`. /// 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. /// 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)? { if !agent_exists(name)? {
bail!("no such agent: '{name}' (no state dir under /var/lib/hyperhive/agents/)"); 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 // `hive-config`); matches `hive-ag3nt::paths::config_dir()`. These
// are the exact files the harness passes to claude each turn. // are the exact files the harness passes to claude each turn.
let cfg = "/run/hive-config"; let cfg = "/run/hive-config";
// A session id (if given) is resumed via `--resume <id>`; 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 // 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 // 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 // a bare session rather than a hard claude error. `name` is
// constrained to `[a-z0-9._-]` so there's nothing to quote. // constrained to `[a-z0-9._-]` so there's nothing to quote. choom
// `--name choom` starts a fresh, named session each launch (claude // never collides with the harness's live session in this same project
// mints its own id), so choom never collides with the harness's live // dir: the harness pins its own session id via `--resume`.
// session in this same project dir; `--resume choom` rejoins it later.
let inner = format!( 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-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-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; \ [ -f {cfg}/claude-system-prompt.md ] && set -- \"$@\" --system-prompt-file {cfg}/claude-system-prompt.md; \