feat(#2067): choom uses a fresh named session, drop --fresh flag

This commit is contained in:
damocles 2026-06-27 23:06:32 +02:00 committed by mara
commit 09b83c9d1e
3 changed files with 41 additions and 114 deletions

View file

@ -110,34 +110,15 @@ enum Cmd {
},
/// Open an interactive Claude session inside an agent container.
///
/// Replaces the current process with `machinectl shell
/// <name>@h-<name>` running claude from the agent's state dir — drops
/// the operator into a private Claude session in the agent's project,
/// with its full settings, MCP tools, and persona. Requires root (same
/// as all machinectl shell operations) and the container must be
/// running.
///
/// To match the harness exactly, choom enters **as the agent user**
/// (not root) so claude reads the OAuth credentials from the agent's
/// `/home/<name>/.claude`; runs from the agent's state dir
/// (`/agents/<name>/state`) so the session is scoped to the right
/// project and `CLAUDE.md` loads; and passes the same `--settings` /
/// `--mcp-config` / `--system-prompt-file` the harness writes to
/// `/run/hive-config/` (settings, MCP tools, role prompt). Entering as
/// root (the `machinectl shell` default) loses all of it.
///
/// choom pins its OWN session id (not a bare `--continue`) so it never
/// resumes or corrupts the harness's live session in the same project
/// dir; re-running choom rejoins that private session. Pass `--fresh`
/// to reset it and start clean.
/// 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.
Choom {
/// Agent name (e.g. `damocles`, `iris`).
name: String,
/// Reset choom's dedicated Claude session and start it clean.
/// Without this flag choom resumes its own prior session (separate
/// from the harness's live session), creating it on first use.
#[arg(long)]
fresh: bool,
},
/// Stop containers hive-wide in one operator action. Bare `hivectl
/// stop` stops **everything** — all sub-agents plus the ci, forge,
@ -644,7 +625,7 @@ async fn main() -> Result<()> {
Cmd::Subvol { cmd } => match cmd {
SubvolCmd::Upgrade { name, yes } => subvol_upgrade(&socket, &name, yes).await,
},
Cmd::Choom { name, fresh } => choom(&name, fresh),
Cmd::Choom { name } => choom(&name),
Cmd::Quota { cmd } => match cmd {
QuotaCmd::Enable => quota_enable().await,
QuotaCmd::Show { name } => quota_show(name.as_deref()).await,
@ -1037,45 +1018,25 @@ fn agent_exists(name: &str) -> Result<bool> {
/// Drop into an interactive Claude session in the agent container.
///
/// Replaces the current process (exec) with `machinectl shell
/// <name>@h-<name> /bin/sh -lc '<script>'`, where the script `cd`s into
/// the agent's state dir and execs claude with the *same* flags the
/// harness uses. Three things matter here, all of which the naive
/// `machinectl shell h-<name> claude` got wrong (issue: choom missing
/// creds + settings):
/// Execs `machinectl shell <name>@h-<name> /bin/sh -lc '<script>'` where
/// the script `cd`s into the agent's state dir and execs claude with the
/// same flags the harness uses. Four things matter:
///
/// 1. **Run as the agent user.** `machinectl shell` defaults to root in
/// the container, so claude would read `/root/.claude` (empty)
/// instead of the agent's `/home/<name>/.claude` where the OAuth
/// credentials live. Prefixing the machine with `<name>@` enters the
/// session as the agent user (the meta-flake sets
/// `hyperhive.user.name` to the agent label, so the unix user name
/// matches the agent name).
/// 2. **Start in the agent's state dir.** A claude session is scoped to
/// the project directory it ran in, and project memory (`CLAUDE.md`)
/// is read from the cwd. The harness runs claude from
/// `/agents/<name>/state`, so choom has to `cd` there or its session +
/// persona resolve against the wrong project.
/// 3. **Pass the harness's claude flags.** The harness drops
/// `claude-{settings.json,mcp-config.json,system-prompt.md}` into
/// `/run/hive-config/` (`hive-ag3nt::paths::config_dir()`) and runs
/// claude with `--settings` / `--mcp-config` / `--system-prompt-file`
/// pointing at them. Skipping them gives the operator default settings,
/// no hyperhive/matrix MCP tools (needed to replay a tool-using
/// history), and no role prompt. choom mirrors those flags; each is
/// added only if the file exists, so a mid-restart container degrades
/// to a bare session instead of erroring.
/// 4. **Pin choom's own session (NOT bare `--continue`).** The harness's
/// live session is pinned to a claude-assigned id in this same project
/// dir; a bare `--continue` would resume the most-recent session there
/// and let choom + the harness clobber each other's history. choom
/// instead uses a fixed sentinel session id (`--resume` it if present,
/// else `--session-id` to create it), so it gets a private session that
/// never collides with the harness's. `--fresh` resets that session.
/// 1. **Run as the agent user** (`<name>@` prefix), not root, so claude
/// reads the agent's `/home/<name>/.claude` OAuth creds.
/// 2. **Start in the agent's state dir** (`/agents/<name>/state`) so the
/// session + `CLAUDE.md` resolve against the right project.
/// 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`.
///
/// `machinectl shell` inherits the caller's PTY, so the session is
/// fully interactive. Requires root and a running container.
fn choom(name: &str, fresh: bool) -> Result<()> {
/// Inherits the caller's PTY. Requires root + a running container.
fn choom(name: &str) -> Result<()> {
if !agent_exists(name)? {
bail!("no such agent: '{name}' (no state dir under /var/lib/hyperhive/agents/)");
}
@ -1092,43 +1053,18 @@ fn choom(name: &str, fresh: bool) -> 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";
// choom pins its OWN claude session under a fixed sentinel id so it
// never resumes — and thus never corrupts — the harness's live session,
// which shares this exact project dir (`state_dir`). The harness session
// id is claude-assigned (random, captured + `--resume`d each turn; see
// hive-ag3nt turn.rs), so it can't collide with this id. A single fixed
// id is safe for every agent because containers are isolated (each has
// its own `$HOME/.claude`).
let choom_session_id = "c0c0c0c0-0000-4000-8000-c0ffeec0ffee";
// `--fresh` drops the stored choom session and starts it clean; the
// default resumes it when a session file for the id exists (so the
// operator rejoins their own prior choom context), creating it on first
// use. `find -name <id>.jsonl` matches regardless of claude's project
// slug, since the id is unique to choom.
let session_setup = if fresh {
format!(
"find \"$HOME/.claude/projects\" -name {choom_session_id}.jsonl -delete 2>/dev/null; \
set -- \"$@\" --session-id {choom_session_id}; "
)
} else {
format!(
"if find \"$HOME/.claude/projects\" -name {choom_session_id}.jsonl -print -quit 2>/dev/null | grep -q .; \
then set -- \"$@\" --resume {choom_session_id}; \
else set -- \"$@\" --session-id {choom_session_id}; fi; "
)
};
// 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.
// `cd` so the pinned session + CLAUDE.md resolve against the agent's
// project; `exec` hands the PTY to claude directly.
// `--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.
let inner = format!(
"cd {state_dir} || exit 1; set --; \
"cd {state_dir} || exit 1; set -- --name choom; \
[ -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; \
{session_setup}\
exec {claude} \"$@\""
);
let mut cmd = std::process::Command::new("machinectl");