feat(#2067): choom --continue passes through to claude --continue (drop --resume translation)

This commit is contained in:
damocles 2026-06-28 23:41:08 +02:00 committed by mara
commit 0125fcfae8
3 changed files with 51 additions and 43 deletions

View file

@ -379,14 +379,17 @@ 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. 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. 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. Bare `choom <name>` launches a fresh blank session; `choom <name> --continue <id|name>` passes `--continue <value>` straight through to claude to rejoin a prior session (claude resolves whether the value is a session id or a display name). It never collides with the harness's live session — the harness pins its own session via `--resume`, so a blank choom session is invisible to it. Requires root + a running container.
**Usage:** `hivectl choom <NAME> [SESSION]` **Usage:** `hivectl choom [OPTIONS] <NAME>`
###### **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
###### **Options:**
* `--continue <SESSION>` — Resume a prior claude session by id or display name, passed through as `claude --continue <value>`. Omit for a fresh blank session. A value is required when the flag is given

View file

@ -127,16 +127,18 @@ 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 blank Claude session 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 hivectl choom iris --continue <id|name> # rejoin a prior session by id or display name
``` ```
With no session id, claude starts a fresh blank session (mints its own Bare `choom` starts a fresh blank session. `--continue <value>` is
id). Pass a session id to resume it (`claude --resume <id>`); you can passed straight through as `claude --continue <value>` to rejoin a
rename it in-session. Either way choom never collides with the harness's prior session — claude resolves whether the value is a session id or a
live session in the same project dir: the harness pins its own id via display name (you can `/rename` a session in-session for an easy
`--resume`, so a blank choom session is invisible to it. The container handle). A value is required when the flag is given. Either way choom
must be running. 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 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,18 +112,21 @@ 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. Without a session id you get a /// / system prompt the harness uses. Bare `choom <name>` launches a
/// fresh blank session each launch; pass one to resume an earlier /// fresh blank session; `choom <name> --continue <id|name>` passes
/// choom session (`--resume <id>`). Either way it never collides with /// `--continue <value>` straight through to claude to rejoin a prior
/// the harness's live session — the harness pins its own session id /// session (claude resolves whether the value is a session id or a
/// via `--resume`, so a blank choom session is invisible to it. /// display name). It never collides with the harness's live session —
/// Requires root + a running container. /// the harness pins its own session 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 /// Resume a prior claude session by id or display name, passed
/// for a fresh blank session. /// through as `claude --continue <value>`. Omit for a fresh
session: Option<String>, /// blank session. A value is required when the flag is given.
#[arg(long = "continue", value_name = "SESSION")]
continue_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,
@ -630,7 +633,10 @@ 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, session } => choom(&name, session.as_deref()), Cmd::Choom {
name,
continue_session,
} => choom(&name, continue_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,
@ -1034,15 +1040,17 @@ 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. **Pick the session explicitly.** With no `session` arg, claude /// 4. **Pick the session explicitly.** Bare choom starts a fresh blank
/// starts a fresh blank session (mints a random id). Pass a session /// session. `--continue <value>` is passed straight through as
/// id to resume it (`--resume <id>`); the operator can rename it /// `claude --continue <value>` to rejoin a prior session — claude
/// in-session. Either way choom never clobbers the harness's live /// resolves whether the value is a session id or a display name (the
/// session: the harness pins its own id via `--resume`, so a blank /// operator can `/rename` a session in-session for an easy handle).
/// choom session in this shared project dir is invisible to it. /// 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. /// Inherits the caller's PTY. Requires root + a running container.
fn choom(name: &str, session: Option<&str>) -> Result<()> { fn choom(name: &str, continue_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/)");
} }
@ -1059,22 +1067,17 @@ fn choom(name: &str, session: Option<&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, // `--continue <value>` (if given) is passed straight through to
// claude starts a fresh blank session. The id is interpolated into // claude; with none, claude starts a fresh blank session. The value
// the shell script, so constrain it to a safe charset first to rule // is single-quoted into the shell script, so reject an embedded
// out injection (real session ids are UUIDs). // single quote (the only char that breaks single-quoting) to rule
let session_arg = match session { // out injection — ids and display names never contain one.
Some(sid) => { let session_arg = match continue_session {
if sid.is_empty() Some(val) => {
|| !sid if val.contains('\'') {
.bytes() bail!("invalid --continue value '{val}': must not contain a single quote");
.all(|b| b.is_ascii_alphanumeric() || b"-_.".contains(&b))
{
bail!(
"invalid session id '{sid}': expected a claude session id (alphanumerics, '-', '_', '.')"
);
} }
format!("set -- --resume {sid};") format!("set -- --continue '{val}';")
} }
None => "set --;".to_string(), None => "set --;".to_string(),
}; };