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

@ -28,6 +28,8 @@ use crate::client;
use crate::events::Bus;
use crate::login::LoginState;
use crate::login_session::{LoginSession, drop_if_finished};
use crate::mcp;
use crate::turn::TurnFiles;
/// Live login state for the web UI. The harness updates this in place as it
/// transitions between `NeedsLogin` and `Online`; the UI reads on each
@ -41,16 +43,25 @@ struct AppState {
session: Arc<Mutex<Option<Arc<LoginSession>>>>,
bus: Bus,
socket: PathBuf,
flavor: Flavor,
/// Same `TurnFiles` the harness's turn loop uses. Shared so
/// `/api/compact` re-uses the exact MCP config / system prompt /
/// settings claude saw on the last regular turn — keeps the
/// session shape identical across compact + normal turns.
files: TurnFiles,
}
impl AppState {
fn flavor(&self) -> Flavor {
self.files.flavor
}
}
/// Which wire protocol the per-agent UI's `/send` handler should speak.
/// Sub-agent → `AgentRequest::OperatorMsg`; manager → `ManagerRequest::OperatorMsg`.
#[derive(Debug, Clone, Copy)]
pub enum Flavor {
Agent,
Manager,
}
/// Sub-agent → `AgentRequest::OperatorMsg`; manager →
/// `ManagerRequest::OperatorMsg`. Reuses the MCP-side enum so a
/// single value drives both the send protocol and (in
/// `post_compact`) the allowed-tools surface claude sees.
pub type Flavor = mcp::Flavor;
pub async fn serve(
label: String,
@ -58,7 +69,7 @@ pub async fn serve(
login: LoginStateCell,
bus: Bus,
socket: PathBuf,
flavor: Flavor,
files: TurnFiles,
) -> Result<()> {
let state = AppState {
label,
@ -66,7 +77,7 @@ pub async fn serve(
session: Arc::new(Mutex::new(None)),
bus,
socket,
flavor,
files,
};
let app = Router::new()
.route("/", get(serve_index))
@ -208,7 +219,7 @@ async fn api_state(State(state): State<AppState>) -> axum::Json<StateSnapshot> {
.ok()
.and_then(|s| s.parse::<u16>().ok())
.unwrap_or(7000);
let inbox = recent_inbox(&state.socket, state.flavor).await;
let inbox = recent_inbox(&state.socket, state.flavor()).await;
let (turn_state, turn_state_since) = state.bus.state_snapshot();
let model = state.bus.model();
axum::Json(StateSnapshot {
@ -268,7 +279,7 @@ async fn post_send(State(state): State<AppState>, Form(form): Form<SendForm>) ->
if body.is_empty() {
return error_response("send: `body` required");
}
let result = match state.flavor {
let result = match state.flavor() {
Flavor::Agent => match client::request::<_, hive_sh4re::AgentResponse>(
&state.socket,
&hive_sh4re::AgentRequest::OperatorMsg { body },
@ -396,22 +407,13 @@ async fn post_set_model(State(state): State<AppState>, Form(form): Form<ModelFor
async fn post_compact(State(state): State<AppState>) -> Response {
let bus = state.bus.clone();
let socket = state.socket.clone();
let files = state.files.clone();
tokio::spawn(async move {
bus.emit(crate::events::LiveEvent::Note(
"operator: /compact — running on persistent session".into(),
));
let settings = match crate::turn::write_settings(&socket).await {
Ok(p) => p,
Err(e) => {
bus.emit(crate::events::LiveEvent::Note(format!(
"/compact failed: settings write — {e:#}"
)));
return;
}
};
bus.set_state(crate::events::TurnState::Compacting);
let r = crate::turn::compact_session(&settings, &bus).await;
let r = crate::turn::compact_session(&files, &bus).await;
bus.set_state(crate::events::TurnState::Idle);
if let Err(e) = r {
bus.emit(crate::events::LiveEvent::Note(format!(