From ce959d7700695b14f9dbf1f50c0ce4be4864b232 Mon Sep 17 00:00:00 2001 From: damocles Date: Sat, 27 Jun 2026 21:21:11 +0200 Subject: [PATCH] feat(#2067): pin choom to a dedicated claude session id --- hive-c0re/src/bin/hivectl.rs | 92 ++++++++++++++++++++++++------------ 1 file changed, 62 insertions(+), 30 deletions(-) diff --git a/hive-c0re/src/bin/hivectl.rs b/hive-c0re/src/bin/hivectl.rs index 6e050532..aa44c9e6 100644 --- a/hive-c0re/src/bin/hivectl.rs +++ b/hive-c0re/src/bin/hivectl.rs @@ -111,29 +111,31 @@ enum Cmd { /// Open an interactive Claude session inside an agent container. /// /// Replaces the current process with `machinectl shell - /// @h-` running `claude --continue` from the agent's - /// state dir — drops the operator straight into the agent's live - /// Claude session with its full loaded context and persona. Requires - /// root (same as all machinectl shell operations) and the container - /// must be running. + /// @h-` 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//.claude`; runs from the agent's state dir - /// (`/agents//state`) so `--continue` resumes the right - /// per-project session 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. + /// (`/agents//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. /// - /// Pass `--fresh` to start a new Claude session instead of continuing - /// the most recent one. + /// 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. Choom { /// Agent name (e.g. `damocles`, `iris`). name: String, - /// Start a fresh Claude session instead of continuing the most - /// recent one. Without this flag `--continue` is passed to Claude - /// so the operator joins the agent's ongoing session context. + /// 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, }, @@ -1049,22 +1051,27 @@ fn agent_exists(name: &str) -> Result { /// 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.** `claude --continue` resumes -/// the most recent session *for the current project directory*, and -/// project memory (`CLAUDE.md`) is read from the cwd. The harness -/// runs claude from `/agents//state`, so choom has to `cd` -/// there or `--continue` finds no session and the persona doesn't -/// load. +/// 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//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. A bare `claude --continue` skips all three, so -/// the operator gets default settings, no hyperhive/matrix MCP tools -/// (which `--continue` needs to replay a tool-using history), and no -/// role prompt. choom now mirrors those flags. Each is added only if -/// the file exists, so a mid-restart container degrades to a bare -/// session instead of erroring. +/// 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. /// /// `machinectl shell` inherits the caller's PTY, so the session is /// fully interactive. Requires root and a running container. @@ -1085,19 +1092,44 @@ 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"; - let continue_flag = if fresh { "" } else { " --continue" }; + // 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 .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 `--continue` + CLAUDE.md resolve against the agent's + // `cd` so the pinned session + CLAUDE.md resolve against the agent's // project; `exec` hands the PTY to claude directly. let inner = format!( "cd {state_dir} || exit 1; set --; \ [ -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; \ - exec {claude} \"$@\"{continue_flag}" + {session_setup}\ + exec {claude} \"$@\"" ); let mut cmd = std::process::Command::new("machinectl"); cmd.arg("shell")