diff --git a/hive-ag3nt/src/bin/hive.rs b/hive-ag3nt/src/bin/hive.rs index 5e8f38ca..da7bfff7 100644 --- a/hive-ag3nt/src/bin/hive.rs +++ b/hive-ag3nt/src/bin/hive.rs @@ -503,6 +503,9 @@ async fn serve_loop( ) -> Result<()> { tracing::info!(socket = %socket.display(), "harness serve"); S::requeue_inflight(socket).await; + // The durable claude session, built once and reused for every turn + + // idle compaction below (it's effectively stateless). + let session = turn::make_session(&bus); // Set when a turn calls `request_next_turn` and no real work is // pending — the next iteration drives this synthetic message // in-process instead of long-polling the broker. Never @@ -519,7 +522,7 @@ async fn serve_loop( // (the in-flight case is handled at the end of drive_turn). let compacted = { let _guard = turn_lock.lock().await; - turn::run_pending_compact(files, &bus).await + turn::run_pending_compact(files, &bus, &session).await }; if !compacted { tokio::time::sleep(interval).await; @@ -545,6 +548,7 @@ async fn serve_loop( stats.as_ref(), files, &turn_lock, + &session, graceful_stop_message(), ) .await; @@ -553,7 +557,8 @@ async fn serve_loop( } }, }; - let ctrl = handle_turn::(socket, &bus, stats.as_ref(), files, &turn_lock, next).await; + let ctrl = + handle_turn::(socket, &bus, stats.as_ref(), files, &turn_lock, &session, next).await; if ctrl.auth_failed { *login_state.lock().unwrap() = LoginState::NeedsLogin; login::wait_for_login( @@ -582,6 +587,7 @@ async fn handle_turn( stats: Option<&TurnStats>, files: &turn::TurnFiles, turn_lock: &TurnLock, + session: &turn::AgentSession, first: hive_sh4re::DeliveredMessage, ) -> TurnControl { let from = first.from; @@ -603,7 +609,7 @@ async fn handle_turn( let prompt = serve_common::format_wake_prompt(msg_id, &from, &body, unread, redelivered); let outcome = { let _guard = turn_lock.lock().await; - turn::drive_turn(&prompt, files, bus).await + turn::drive_turn(&prompt, files, bus, session).await }; turn::emit_turn_end(bus, &outcome); bus.set_state(TurnState::Idle); diff --git a/hive-ag3nt/src/turn.rs b/hive-ag3nt/src/turn.rs index 4f2455ce..00451a9b 100644 --- a/hive-ag3nt/src/turn.rs +++ b/hive-ag3nt/src/turn.rs @@ -230,11 +230,20 @@ fn compact_percent() -> u8 { u8::try_from(pct.min(100)).unwrap_or(DEFAULT_COMPACT_PERCENT) } -/// Build the agent's durable session: constant title + on-disk store + a +/// The agent's durable session type: the constant-title [`InfiniteSession`] +/// with hyperhive's percent-of-window compaction policy. Built once by the +/// serve loop (see [`make_session`]) and threaded through the turns, rather +/// than rebuilt each time — it's effectively stateless, so one instance serves +/// the whole run. +pub type AgentSession = InfiniteSession; + +/// Construct the agent's durable session: constant title + on-disk store + a /// percent-of-window compaction policy that checkpoints (`CHECKPOINT_PROMPT`) -/// before compacting. `default_window` feeds the policy the effective window -/// for turns where the model didn't itself report one. -fn infinite_session(bus: &Bus) -> InfiniteSession { +/// before compacting. Called once at serve-loop start. `percent` comes from a +/// boot-time env var and `default_window` is only a fallback for turns where +/// the model didn't report a window, so a single build at startup is fine. +#[must_use] +pub fn make_session(bus: &Bus) -> AgentSession { InfiniteSession::new( session_title(), session_store(), @@ -260,8 +269,14 @@ fn infinite_session(bus: &Bus) -> InfiniteSession { /// the whole turn is retried a single time before bubbling `AuthFailed` to /// the serve loop (which parks for re-login). /// -/// Called once per turn by the `hive` serve loop. -pub async fn drive_turn(prompt: &str, files: &TurnFiles, bus: &Bus) -> TurnOutcome { +/// Called once per turn by the `hive` serve loop, which owns the shared +/// `session` ([`make_session`]) and threads it in. +pub async fn drive_turn( + prompt: &str, + files: &TurnFiles, + bus: &Bus, + session: &AgentSession, +) -> TurnOutcome { if bus.take_session_reset() { // Operator-requested (deferred from `POST /api/new-session`). bus.emit(LiveEvent::Note { @@ -274,7 +289,6 @@ pub async fn drive_turn(prompt: &str, files: &TurnFiles, bus: &Bus) -> TurnOutco } let config = claude_config(bus, files); let sink = BusSink::new(bus); - let session = infinite_session(bus); let mut result = session.run(&config, prompt, &sink).await; if matches!(result, Err(hive_claude::Error::AuthFailed)) { bus.emit(LiveEvent::Note { @@ -398,7 +412,7 @@ pub fn emit_turn_end(bus: &Bus, outcome: &TurnOutcome) { /// case is handled at the end of [`drive_turn`].) Resume-only via /// [`InfiniteSession::compact`]: a missing session is a harmless no-op. Returns /// `true` if a compaction ran. -pub async fn run_pending_compact(files: &TurnFiles, bus: &Bus) -> bool { +pub async fn run_pending_compact(files: &TurnFiles, bus: &Bus, session: &AgentSession) -> bool { if !bus.take_compact() { return false; } @@ -408,7 +422,7 @@ pub async fn run_pending_compact(files: &TurnFiles, bus: &Bus) -> bool { bus.set_state(crate::events::TurnState::Compacting); let config = claude_config(bus, files); let sink = BusSink::new(bus); - match infinite_session(bus).compact(&config, &sink).await { + match session.compact(&config, &sink).await { Ok(()) => bus.emit(LiveEvent::Note { text: "/compact done".into(), }),