hive-agent: guarantee a wake after a self-requested /compact
This commit is contained in:
parent
fa658567db
commit
2c7872841a
8 changed files with 227 additions and 48 deletions
|
|
@ -240,6 +240,21 @@ fn synthetic_todo_message(stern: bool) -> hive_sh4re::inbox::DeliveredMessage {
|
|||
}
|
||||
}
|
||||
|
||||
/// Synthetic message that drives the follow-up turn when a `/compact` call
|
||||
/// (self-requested via the `compact` MCP tool's `wake_prompt` arg) finishes
|
||||
/// and asked to be woken. Mirrors `synthetic_todo_message`'s "no broker row"
|
||||
/// sentinel shape (`id = 0`) — the compact tool call itself is the durable
|
||||
/// record that a wake was requested, not a broker message.
|
||||
fn post_compact_wake_message(prompt: String) -> hive_sh4re::inbox::DeliveredMessage {
|
||||
hive_sh4re::inbox::DeliveredMessage {
|
||||
from: "compact".into(),
|
||||
body: prompt,
|
||||
id: 0,
|
||||
redelivered: false,
|
||||
in_reply_to: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Synthetic message that drives the single stop-checkpoint turn when c0re
|
||||
/// signals a graceful stop. The agent gets one final turn to flush durable
|
||||
/// `/state` before the container is stopped; new inbound is already fenced.
|
||||
|
|
@ -772,8 +787,18 @@ async fn serve_loop<S: Surface>(
|
|||
let compacted = turn::run_pending_compact(files, &bus, &session).await;
|
||||
if !compacted {
|
||||
tokio::time::sleep(interval).await;
|
||||
continue;
|
||||
}
|
||||
continue;
|
||||
// The compact that just ran may have carried a wake prompt
|
||||
// (the agent's own `compact` tool, not the operator button —
|
||||
// see `Bus::request_compact`). If so, drive it as a synthetic
|
||||
// turn right now instead of looping back to `recv_next` and
|
||||
// waiting for the next external event.
|
||||
let Some(prompt) = bus.take_post_compact_wake() else {
|
||||
continue;
|
||||
};
|
||||
tracing::debug!("post-compact wake queued, driving synthetic follow-up turn");
|
||||
post_compact_wake_message(prompt)
|
||||
}
|
||||
RecvOutcome::TransportError => {
|
||||
// `recv_next` already logged the detail; just retry.
|
||||
|
|
@ -802,32 +827,83 @@ async fn serve_loop<S: Surface>(
|
|||
return Ok(());
|
||||
}
|
||||
};
|
||||
let ctrl = handle_turn::<S>(
|
||||
let turn_ctx = TurnCtx {
|
||||
socket,
|
||||
&bus,
|
||||
stats.as_ref(),
|
||||
bus: &bus,
|
||||
stats: stats.as_ref(),
|
||||
files,
|
||||
&session,
|
||||
session: &session,
|
||||
interrupted: &interrupted,
|
||||
login_state: &login_state,
|
||||
claude_dir: &claude_dir,
|
||||
interval,
|
||||
};
|
||||
drive_turn_and_wake_chain::<S>(&turn_ctx, next, &mut todo_miss_streak).await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Loop-invariant turn-driving context for `drive_turn_and_wake_chain`,
|
||||
/// threaded as one bundle instead of double-digit positional args
|
||||
/// (clippy's `too_many_arguments`). Everything here is constant for the
|
||||
/// lifetime of one `serve_loop` call; only the message to drive and the
|
||||
/// todo-miss streak vary per turn and stay as separate params.
|
||||
struct TurnCtx<'a> {
|
||||
socket: &'a Path,
|
||||
bus: &'a Bus,
|
||||
stats: Option<&'a TurnStats>,
|
||||
files: &'a turn::TurnFiles,
|
||||
session: &'a turn::AgentSession,
|
||||
interrupted: &'a Arc<std::sync::atomic::AtomicBool>,
|
||||
login_state: &'a Arc<Mutex<LoginState>>,
|
||||
claude_dir: &'a Path,
|
||||
interval: Duration,
|
||||
}
|
||||
|
||||
/// Drive `next`, then keep driving synthetic follow-up turns for as long as
|
||||
/// a compact that just ran carries a wake prompt
|
||||
/// (`Bus::take_post_compact_wake`) — see `serve_loop`'s comment at the call
|
||||
/// site for why this loops in place instead of returning to the outer
|
||||
/// `select!`/`recv_next`. Ordinarily runs exactly one iteration; only chains
|
||||
/// further if the follow-up turn itself requests another woken compact.
|
||||
/// Split out of `serve_loop` purely to keep that function under clippy's
|
||||
/// line limit.
|
||||
async fn drive_turn_and_wake_chain<S: Surface>(
|
||||
ctx: &TurnCtx<'_>,
|
||||
mut next: hive_sh4re::inbox::DeliveredMessage,
|
||||
todo_miss_streak: &mut u32,
|
||||
) {
|
||||
loop {
|
||||
let ctrl = handle_turn::<S>(
|
||||
ctx.socket,
|
||||
ctx.bus,
|
||||
ctx.stats,
|
||||
ctx.files,
|
||||
ctx.session,
|
||||
next,
|
||||
&interrupted,
|
||||
ctx.interrupted,
|
||||
)
|
||||
.await;
|
||||
apply_todo_wake_checked(ctrl.todo_wake_checked, &mut todo_miss_streak, &bus);
|
||||
apply_todo_wake_checked(ctrl.todo_wake_checked, todo_miss_streak, ctx.bus);
|
||||
if ctrl.auth_failed {
|
||||
*login_state.lock().unwrap() = LoginState::NeedsLogin;
|
||||
*ctx.login_state.lock().unwrap() = LoginState::NeedsLogin;
|
||||
// Baseline the resume check on *this instant*, not on a
|
||||
// directory snapshot taken after `wait_for_login` starts
|
||||
// polling — closes the race where a login lands between the
|
||||
// 401 and the first poll. See `wait_for_login`'s doc comment.
|
||||
login::wait_for_login(
|
||||
&claude_dir,
|
||||
login_state.clone(),
|
||||
&bus,
|
||||
u64::try_from(interval.as_millis()).unwrap_or(2000),
|
||||
ctx.claude_dir,
|
||||
ctx.login_state.clone(),
|
||||
ctx.bus,
|
||||
u64::try_from(ctx.interval.as_millis()).unwrap_or(2000),
|
||||
std::time::SystemTime::now(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
let Some(prompt) = ctx.bus.take_post_compact_wake() else {
|
||||
break;
|
||||
};
|
||||
tracing::debug!("post-compact wake queued mid-turn-flow, driving synthetic follow-up turn");
|
||||
next = post_compact_wake_message(prompt);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue