turn: unify run_turn / compact_session via TurnFiles

new TurnFiles bundle (mcp_config + settings + system_prompt +
flavor) materialised once per harness boot, passed to drive_turn
and compact_session alike. operator-initiated /compact now uses
the exact same session shape as a normal turn — same MCP
surface, same allowed tools, same role prompt — only the stdin
payload differs (/compact vs the wake-up body). web_ui's
AppState carries the TurnFiles instead of (label + socket +
flavor + ad-hoc file writes per click). bin/hive-ag3nt and
bin/hive-m1nd prepare TurnFiles before spawning the web UI and
pass them to both surfaces. web_ui::Flavor folds into a type
alias for mcp::Flavor — no two-stage enum mapping.

removes ClaudeMode + the run_claude variant fork (system prompt
was Option, mcp args were skipped on Compact). dead 'mode'
plumbing gone.
This commit is contained in:
müde 2026-05-16 00:57:58 +02:00
parent 87c7b05b05
commit d94712bde8
4 changed files with 109 additions and 163 deletions

View file

@ -33,6 +33,34 @@ const CLAUDE_SETTINGS: &str = include_str!("../prompts/claude-settings.json");
/// claude exit with a useful error in the live view.
const PROMPT_TOO_LONG_MARKER: &str = "Prompt is too long";
/// The set of files claude reads on every invocation: the MCP server
/// config (`--mcp-config`), static settings (`--settings`), and the
/// pre-rendered role/tools system prompt (`--system-prompt-file`).
/// Materialised once at harness startup; shared between the turn loop
/// and the operator-driven `/compact` path so both invocations look
/// identical to claude (same MCP surface, same allowed tools, same
/// role prompt — only the stdin payload differs).
#[derive(Clone)]
pub struct TurnFiles {
pub mcp_config: PathBuf,
pub settings: PathBuf,
pub system_prompt: PathBuf,
pub flavor: mcp::Flavor,
}
impl TurnFiles {
/// Write all three files into the per-agent runtime dir alongside
/// `socket`. Idempotent — overwrites whatever was there.
pub async fn prepare(socket: &Path, label: &str, flavor: mcp::Flavor) -> Result<Self> {
Ok(Self {
mcp_config: write_mcp_config(socket).await?,
settings: write_settings(socket).await?,
system_prompt: write_system_prompt(socket, label, flavor).await?,
flavor,
})
}
}
/// Drop the MCP config blob claude reads from `--mcp-config <path>`.
/// `socket` is the hyperhive per-container socket (forwarded to the child
/// as `--socket <path>`); `binary_subcommand` is e.g. `"mcp"` for sub-agents
@ -99,21 +127,14 @@ pub enum TurnOutcome {
/// Drive one turn end-to-end, transparently compacting + retrying once on
/// `Prompt is too long`. Both the sub-agent and manager loops call this.
pub async fn drive_turn(
prompt: &str,
mcp_config: &Path,
system_prompt: &Path,
settings: &Path,
bus: &Bus,
flavor: mcp::Flavor,
) -> TurnOutcome {
match run_turn(prompt, mcp_config, system_prompt, settings, bus, flavor).await {
pub async fn drive_turn(prompt: &str, files: &TurnFiles, bus: &Bus) -> TurnOutcome {
match run_turn(prompt, files, bus).await {
TurnOutcome::PromptTooLong => {
if let Err(e) = compact_session(settings, bus).await {
if let Err(e) = compact_session(files, bus).await {
tracing::warn!(error = %format!("{e:#}"), "compact failed");
return TurnOutcome::Failed(e);
}
run_turn(prompt, mcp_config, system_prompt, settings, bus, flavor).await
run_turn(prompt, files, bus).await
}
other => other,
}
@ -166,25 +187,8 @@ pub async fn wait_for_login(claude_dir: &Path, state: Arc<Mutex<LoginState>>, po
/// prompt). The session is persistent across turns via `--continue` and
/// claude's in-session auto-compact is disabled via `--settings` so it
/// doesn't stall mid-turn — hyperhive owns compaction.
pub async fn run_turn(
prompt: &str,
mcp_config: &Path,
system_prompt: &Path,
settings: &Path,
bus: &Bus,
flavor: mcp::Flavor,
) -> TurnOutcome {
match run_claude(
prompt,
mcp_config,
Some(system_prompt),
settings,
bus,
flavor,
ClaudeMode::Turn,
)
.await
{
pub async fn run_turn(prompt: &str, files: &TurnFiles, bus: &Bus) -> TurnOutcome {
match run_claude(prompt, files, bus).await {
Ok(too_long) if too_long => TurnOutcome::PromptTooLong,
Ok(_) => TurnOutcome::Ok,
Err(e) => TurnOutcome::Failed(e),
@ -192,49 +196,23 @@ pub async fn run_turn(
}
/// Run claude's built-in `/compact` slash command on the persistent
/// session so the next turn can fit. No MCP tools needed; we just feed
/// `/compact` over stdin and let claude rewrite its own history.
pub async fn compact_session(settings: &Path, bus: &Bus) -> Result<()> {
/// session. Takes the *same* params as `run_turn` because compact
/// re-initialises claude with the full session shape — same MCP
/// surface, same system prompt, same allowed-tools — so the post-
/// compact state matches a normal turn's. Only the prompt over stdin
/// differs (`/compact` vs the wake-up payload).
pub async fn compact_session(files: &TurnFiles, bus: &Bus) -> Result<()> {
bus.emit(LiveEvent::Note(
"context overflow — running /compact on the persistent session".into(),
));
let _ = run_claude(
"/compact",
Path::new("/dev/null"),
None,
settings,
bus,
mcp::Flavor::Agent, // tool surface unused for /compact
ClaudeMode::Compact,
)
.await?;
let _ = run_claude("/compact", files, bus).await?;
bus.emit(LiveEvent::Note("/compact done".into()));
Ok(())
}
#[derive(Clone, Copy)]
enum ClaudeMode {
Turn,
Compact,
}
async fn run_claude(
prompt: &str,
mcp_config: &Path,
system_prompt: Option<&Path>,
settings: &Path,
bus: &Bus,
flavor: mcp::Flavor,
mode: ClaudeMode,
) -> Result<bool> {
async fn run_claude(prompt: &str, files: &TurnFiles, bus: &Bus) -> Result<bool> {
let model = bus.model();
// /compact must always run against the existing session — otherwise
// there's nothing to compact. Only normal turns honor the
// operator's "new session" one-shot flag.
let resume = match mode {
ClaudeMode::Turn => !bus.take_skip_continue(),
ClaudeMode::Compact => true,
};
let resume = !bus.take_skip_continue();
if !resume {
bus.emit(LiveEvent::Note(
"fresh session (--continue suppressed for this turn)".into(),
@ -258,22 +236,18 @@ async fn run_claude(
.arg("--model")
.arg(&model)
.arg("--settings")
.arg(settings);
.arg(&files.settings);
if resume {
cmd.arg("--continue");
}
if let Some(p) = system_prompt {
cmd.arg("--system-prompt-file").arg(p);
}
if let ClaudeMode::Turn = mode {
cmd.arg("--mcp-config")
.arg(mcp_config)
.arg("--strict-mcp-config")
.arg("--tools")
.arg(mcp::builtin_tools_arg())
.arg("--allowedTools")
.arg(mcp::allowed_tools_arg(flavor));
}
cmd.arg("--system-prompt-file").arg(&files.system_prompt);
cmd.arg("--mcp-config")
.arg(&files.mcp_config)
.arg("--strict-mcp-config")
.arg("--tools")
.arg(mcp::builtin_tools_arg())
.arg("--allowedTools")
.arg(mcp::allowed_tools_arg(files.flavor));
let mut child = cmd
.stdin(Stdio::piped())
.stdout(Stdio::piped())