refactor: split long functions per review feedback; remove all #[allow] attributes
This commit is contained in:
parent
bbe2112dc9
commit
748536203b
5 changed files with 429 additions and 432 deletions
|
|
@ -149,11 +149,11 @@ async fn main() -> Result<()> {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments, clippy::similar_names, clippy::too_many_lines)]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn serve(
|
||||
socket: &Path,
|
||||
interval: Duration,
|
||||
state: Arc<Mutex<LoginState>>,
|
||||
_login_state: Arc<Mutex<LoginState>>,
|
||||
bus: Bus,
|
||||
stats: Option<TurnStats>,
|
||||
files: &turn::TurnFiles,
|
||||
|
|
@ -161,25 +161,12 @@ async fn serve(
|
|||
label: &str,
|
||||
) -> Result<()> {
|
||||
tracing::info!(socket = %socket.display(), "hive-ag3nt serve");
|
||||
let _ = state; // reserved for future state transitions (turn-loop -> needs-login)
|
||||
// Boot-time recovery: ask the broker to resurface anything we
|
||||
// popped in a previous harness session but never acked
|
||||
// (crashed mid-turn / OOM / container restart). The broker
|
||||
// resets `delivered_at = NULL` on those rows and remembers
|
||||
// their ids so the next `Recv` tags them `redelivered: true`;
|
||||
// we then prepend a "may already be handled" hint to the wake
|
||||
// prompt. Single shot before entering the serve loop; idempotent
|
||||
// when there's nothing inflight.
|
||||
requeue_inflight(socket).await;
|
||||
loop {
|
||||
let recv: Result<AgentResponse> =
|
||||
// Explicit long-poll: the new agent_server semantics treat
|
||||
// `None` as "peek, don't wait", which would tight-loop on
|
||||
// sleep(interval). The harness wants to park until a
|
||||
// message arrives, so opt into the full 180s cap.
|
||||
// `max: None` (= 1) — the serve loop drives one turn per
|
||||
// wake; claude itself calls recv(max: N) in-turn to drain
|
||||
// a burst when the wake prompt mentions pending.
|
||||
// Explicit long-poll: park until a message arrives (180s cap).
|
||||
// `max: None` (= 1) — one turn per wake; claude calls
|
||||
// recv(max: N) in-turn to drain bursts.
|
||||
client::request(
|
||||
socket,
|
||||
&AgentRequest::Recv {
|
||||
|
|
@ -191,93 +178,7 @@ async fn serve(
|
|||
match recv {
|
||||
Ok(AgentResponse::Messages { messages }) if !messages.is_empty() => {
|
||||
let first = messages.into_iter().next().expect("checked non-empty");
|
||||
let from = first.from;
|
||||
let body = first.body;
|
||||
let redelivered = first.redelivered;
|
||||
tracing::info!(%from, %body, %redelivered, "inbox");
|
||||
let unread = inbox_unread(socket).await;
|
||||
bus.emit(LiveEvent::TurnStart {
|
||||
from: from.clone(),
|
||||
body: body.clone(),
|
||||
unread,
|
||||
});
|
||||
bus.set_state(TurnState::Thinking);
|
||||
let started_at = serve_common::now_unix();
|
||||
let started_instant = std::time::Instant::now();
|
||||
let model_at_start = bus.model();
|
||||
let prompt = serve_common::format_wake_prompt(&from, &body, unread, redelivered);
|
||||
let outcome = {
|
||||
let _guard = turn_lock.lock().await;
|
||||
turn::drive_turn(&prompt, files, &bus).await
|
||||
};
|
||||
turn::emit_turn_end(&bus, &outcome);
|
||||
bus.set_state(TurnState::Idle);
|
||||
// Ack only on a clean turn-end. `Failed` leaves every
|
||||
// message popped during the turn in the unacked list;
|
||||
// next harness boot's `RequeueInflight` will reset
|
||||
// `delivered_at = NULL` and tag them `redelivered`.
|
||||
// `PromptTooLong` is absorbed inside `drive_turn` via
|
||||
// compaction so it shouldn't reach here, but if it
|
||||
// does we also skip the ack (safer to redeliver than
|
||||
// to lose the message).
|
||||
if matches!(outcome, turn::TurnOutcome::Ok | turn::TurnOutcome::Compacted) {
|
||||
ack_turn(socket).await;
|
||||
}
|
||||
// Rate-limited: park until the quota resets, then requeue
|
||||
// the unacked message so it resurfaces in the same session.
|
||||
if matches!(outcome, turn::TurnOutcome::RateLimited) {
|
||||
let secs = turn::rate_limit_sleep_secs();
|
||||
bus.emit_status("rate_limited");
|
||||
bus.emit(LiveEvent::Note {
|
||||
text: format!(
|
||||
"API rate-limited — sleeping {secs}s before retry"
|
||||
),
|
||||
});
|
||||
tracing::warn!(sleep_secs = secs, "rate-limited; parking");
|
||||
tokio::time::sleep(Duration::from_secs(secs)).await;
|
||||
requeue_inflight(socket).await;
|
||||
bus.emit_status("online");
|
||||
}
|
||||
// Failures are unhandled by definition — PromptTooLong is
|
||||
// absorbed inside drive_turn via compaction, so anything
|
||||
// that reaches Failed here is a real crash. Notify the
|
||||
// manager so it can investigate / restart / page the
|
||||
// operator; best-effort, swallow the send error.
|
||||
if let turn::TurnOutcome::Failed(e) = &outcome {
|
||||
notify_manager_of_failure(socket, label, e).await;
|
||||
}
|
||||
if let Some(s) = &stats {
|
||||
let ended_at = serve_common::now_unix();
|
||||
let duration_ms =
|
||||
i64::try_from(started_instant.elapsed().as_millis()).unwrap_or(i64::MAX);
|
||||
let (open_threads, open_reminders) = fetch_agent_post_turn_counts(socket).await;
|
||||
let row = serve_common::build_row(
|
||||
started_at,
|
||||
ended_at,
|
||||
duration_ms,
|
||||
model_at_start,
|
||||
from.clone(),
|
||||
&outcome,
|
||||
&bus,
|
||||
open_threads,
|
||||
open_reminders,
|
||||
);
|
||||
s.record(&row);
|
||||
}
|
||||
|
||||
// After turn completes, log whether messages arrived during
|
||||
// the turn — the outer loop will iterate back to recv() on
|
||||
// its own (the Empty-arm sleep only fires when recv
|
||||
// actually returned Empty), so no explicit continue needed.
|
||||
let pending = inbox_unread(socket).await;
|
||||
if pending > 0 {
|
||||
tracing::info!(%pending, "pending messages after turn; fetching next");
|
||||
}
|
||||
// `request_next_turn` MCP tool: agent wrote a sentinel
|
||||
// requesting an immediate self-continuation turn. Clear
|
||||
// the file and inject a synthetic wake so the outer loop
|
||||
// fires a bare turn even if the inbox is empty.
|
||||
check_and_inject_continue(socket, label).await;
|
||||
handle_agent_turn(socket, &bus, stats.as_ref(), files, &turn_lock, label, first).await;
|
||||
}
|
||||
Ok(AgentResponse::Messages { .. }) => {
|
||||
// Idle: empty list = nothing pending. Brief sleep
|
||||
|
|
@ -307,6 +208,80 @@ async fn serve(
|
|||
}
|
||||
}
|
||||
|
||||
/// Drive one turn for a received agent-inbox message.
|
||||
async fn handle_agent_turn(
|
||||
socket: &Path,
|
||||
bus: &Bus,
|
||||
stats: Option<&TurnStats>,
|
||||
files: &turn::TurnFiles,
|
||||
turn_lock: &TurnLock,
|
||||
label: &str,
|
||||
first: hive_sh4re::DeliveredMessage,
|
||||
) {
|
||||
let from = first.from;
|
||||
let body = first.body;
|
||||
let redelivered = first.redelivered;
|
||||
tracing::info!(%from, %body, %redelivered, "inbox");
|
||||
let unread = inbox_unread(socket).await;
|
||||
bus.emit(LiveEvent::TurnStart { from: from.clone(), body: body.clone(), unread });
|
||||
bus.set_state(TurnState::Thinking);
|
||||
let started_at = serve_common::now_unix();
|
||||
let started_instant = std::time::Instant::now();
|
||||
let model_at_start = bus.model();
|
||||
let prompt = serve_common::format_wake_prompt(&from, &body, unread, redelivered);
|
||||
let outcome = {
|
||||
let _guard = turn_lock.lock().await;
|
||||
turn::drive_turn(&prompt, files, bus).await
|
||||
};
|
||||
turn::emit_turn_end(bus, &outcome);
|
||||
bus.set_state(TurnState::Idle);
|
||||
// Ack only on a clean turn-end. `Failed` leaves every message popped
|
||||
// during the turn in the unacked list; next harness boot requeues them.
|
||||
if matches!(outcome, turn::TurnOutcome::Ok | turn::TurnOutcome::Compacted) {
|
||||
ack_turn(socket).await;
|
||||
}
|
||||
if matches!(outcome, turn::TurnOutcome::RateLimited) {
|
||||
let secs = turn::rate_limit_sleep_secs();
|
||||
bus.emit_status("rate_limited");
|
||||
bus.emit(LiveEvent::Note {
|
||||
text: format!("API rate-limited — sleeping {secs}s before retry"),
|
||||
});
|
||||
tracing::warn!(sleep_secs = secs, "rate-limited; parking");
|
||||
tokio::time::sleep(Duration::from_secs(secs)).await;
|
||||
requeue_inflight(socket).await;
|
||||
bus.emit_status("online");
|
||||
}
|
||||
// Real crash: PromptTooLong is absorbed by compaction inside drive_turn.
|
||||
if let turn::TurnOutcome::Failed(e) = &outcome {
|
||||
notify_manager_of_failure(socket, label, e).await;
|
||||
}
|
||||
if let Some(stats) = stats {
|
||||
let ended_at = serve_common::now_unix();
|
||||
let duration_ms =
|
||||
i64::try_from(started_instant.elapsed().as_millis()).unwrap_or(i64::MAX);
|
||||
let (open_threads, open_reminders) = fetch_agent_post_turn_counts(socket).await;
|
||||
let row = serve_common::build_row(
|
||||
started_at,
|
||||
ended_at,
|
||||
duration_ms,
|
||||
model_at_start,
|
||||
from.clone(),
|
||||
&outcome,
|
||||
bus,
|
||||
open_threads,
|
||||
open_reminders,
|
||||
);
|
||||
stats.record(&row);
|
||||
}
|
||||
let pending = inbox_unread(socket).await;
|
||||
if pending > 0 {
|
||||
tracing::info!(%pending, "pending messages after turn; fetching next");
|
||||
}
|
||||
// `request_next_turn` MCP tool: agent wrote a sentinel requesting
|
||||
// an immediate self-continuation. Clear and inject synthetic wake.
|
||||
check_and_inject_continue(socket, label).await;
|
||||
}
|
||||
|
||||
// Per-turn user prompt: the role/tools/etc. is in the system prompt
|
||||
// (`prompts/agent.md` → `claude --system-prompt-file`); this is just the
|
||||
// wake signal claude reacts to. `unread` is the count of *other*
|
||||
|
|
|
|||
|
|
@ -113,7 +113,6 @@ async fn main() -> Result<()> {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)] // linear startup sequence; splitting adds indirection without clarity
|
||||
async fn serve(
|
||||
socket: &Path,
|
||||
interval: Duration,
|
||||
|
|
@ -145,93 +144,7 @@ async fn serve(
|
|||
match recv {
|
||||
Ok(ManagerResponse::Messages { messages }) if !messages.is_empty() => {
|
||||
let first = messages.into_iter().next().expect("checked non-empty");
|
||||
let from = first.from;
|
||||
let body = first.body;
|
||||
let redelivered = first.redelivered;
|
||||
if from == SYSTEM_SENDER {
|
||||
// Helper events (ApprovalResolved / Spawned / Rebuilt /
|
||||
// Killed / Destroyed) — these are FYI for the manager;
|
||||
// we surface them in the live view and forward them as
|
||||
// a normal claude turn so the manager can react (e.g.
|
||||
// greet a newly-spawned agent, retry a failed rebuild).
|
||||
let parsed = serde_json::from_str::<HelperEvent>(&body).ok();
|
||||
if let Some(event) = parsed {
|
||||
tracing::info!(?event, "helper event");
|
||||
} else {
|
||||
tracing::info!(%from, %body, "system message");
|
||||
}
|
||||
bus.emit(LiveEvent::Note {
|
||||
text: format!("[system] {body}"),
|
||||
});
|
||||
// Fall through: drive a turn with the event in the wake
|
||||
// prompt body so claude sees it. Sender stays "system"
|
||||
// so the wake prompt can label it as such.
|
||||
}
|
||||
tracing::info!(%from, %body, %redelivered, "manager inbox");
|
||||
let unread = inbox_unread(socket).await;
|
||||
bus.emit(LiveEvent::TurnStart {
|
||||
from: from.clone(),
|
||||
body: body.clone(),
|
||||
unread,
|
||||
});
|
||||
let prompt = serve_common::format_wake_prompt(&from, &body, unread, redelivered);
|
||||
bus.set_state(TurnState::Thinking);
|
||||
let started_at = serve_common::now_unix();
|
||||
let started_instant = std::time::Instant::now();
|
||||
let model_at_start = bus.model();
|
||||
let outcome = {
|
||||
let _guard = turn_lock.lock().await;
|
||||
turn::drive_turn(&prompt, files, &bus).await
|
||||
};
|
||||
turn::emit_turn_end(&bus, &outcome);
|
||||
bus.set_state(TurnState::Idle);
|
||||
// Ack only on a clean turn-end; Failed / RateLimited leave
|
||||
// the popped ids in-flight for the next boot's requeue.
|
||||
// Mirrors hive-ag3nt; see that loop for full rationale.
|
||||
if matches!(outcome, turn::TurnOutcome::Ok | turn::TurnOutcome::Compacted) {
|
||||
ack_turn(socket).await;
|
||||
}
|
||||
// Rate-limited: park until the quota resets, then requeue
|
||||
// the unacked message so it resurfaces in the same session.
|
||||
if matches!(outcome, turn::TurnOutcome::RateLimited) {
|
||||
let secs = turn::rate_limit_sleep_secs();
|
||||
bus.emit_status("rate_limited");
|
||||
bus.emit(LiveEvent::Note {
|
||||
text: format!(
|
||||
"API rate-limited — sleeping {secs}s before retry"
|
||||
),
|
||||
});
|
||||
tracing::warn!(sleep_secs = secs, "rate-limited; parking");
|
||||
tokio::time::sleep(Duration::from_secs(secs)).await;
|
||||
requeue_inflight(socket).await;
|
||||
bus.emit_status("online");
|
||||
}
|
||||
if let Some(s) = &stats {
|
||||
let ended_at = serve_common::now_unix();
|
||||
let duration_ms =
|
||||
i64::try_from(started_instant.elapsed().as_millis()).unwrap_or(i64::MAX);
|
||||
let (open_threads, open_reminders) =
|
||||
fetch_manager_post_turn_counts(socket).await;
|
||||
let row = serve_common::build_row(
|
||||
started_at,
|
||||
ended_at,
|
||||
duration_ms,
|
||||
model_at_start,
|
||||
from.clone(),
|
||||
&outcome,
|
||||
&bus,
|
||||
open_threads,
|
||||
open_reminders,
|
||||
);
|
||||
s.record(&row);
|
||||
}
|
||||
// Check for messages that arrived during the turn so we
|
||||
// surface "draining" in the logs. The loop will already
|
||||
// re-iterate from here — no explicit continue needed.
|
||||
let pending = inbox_unread(socket).await;
|
||||
if pending > 0 {
|
||||
tracing::info!(%pending, "pending messages after turn; fetching next");
|
||||
}
|
||||
handle_manager_turn(socket, &bus, stats.as_ref(), files, &turn_lock, first).await;
|
||||
}
|
||||
Ok(ManagerResponse::Messages { .. }) => {
|
||||
// Idle: empty list = nothing pending. Brief sleep
|
||||
|
|
@ -261,6 +174,84 @@ async fn serve(
|
|||
}
|
||||
}
|
||||
|
||||
/// Drive one turn for a received manager-inbox message. Called from the
|
||||
/// serve loop for the non-empty-messages arm to keep that loop readable.
|
||||
async fn handle_manager_turn(
|
||||
socket: &Path,
|
||||
bus: &Bus,
|
||||
stats: Option<&TurnStats>,
|
||||
files: &turn::TurnFiles,
|
||||
turn_lock: &TurnLock,
|
||||
first: hive_sh4re::DeliveredMessage,
|
||||
) {
|
||||
let from = first.from;
|
||||
let body = first.body;
|
||||
let redelivered = first.redelivered;
|
||||
if from == SYSTEM_SENDER {
|
||||
// Helper events (ApprovalResolved / Spawned / Rebuilt /
|
||||
// Killed / Destroyed) — surface in the live view and drive a
|
||||
// normal turn so the manager can react.
|
||||
let parsed = serde_json::from_str::<HelperEvent>(&body).ok();
|
||||
if let Some(event) = parsed {
|
||||
tracing::info!(?event, "helper event");
|
||||
} else {
|
||||
tracing::info!(%from, %body, "system message");
|
||||
}
|
||||
bus.emit(LiveEvent::Note { text: format!("[system] {body}") });
|
||||
}
|
||||
tracing::info!(%from, %body, %redelivered, "manager inbox");
|
||||
let unread = inbox_unread(socket).await;
|
||||
bus.emit(LiveEvent::TurnStart { from: from.clone(), body: body.clone(), unread });
|
||||
let prompt = serve_common::format_wake_prompt(&from, &body, unread, redelivered);
|
||||
bus.set_state(TurnState::Thinking);
|
||||
let started_at = serve_common::now_unix();
|
||||
let started_instant = std::time::Instant::now();
|
||||
let model_at_start = bus.model();
|
||||
let outcome = {
|
||||
let _guard = turn_lock.lock().await;
|
||||
turn::drive_turn(&prompt, files, bus).await
|
||||
};
|
||||
turn::emit_turn_end(bus, &outcome);
|
||||
bus.set_state(TurnState::Idle);
|
||||
// Ack only on a clean turn-end; Failed / RateLimited leave the
|
||||
// popped ids in-flight for the next boot's requeue.
|
||||
if matches!(outcome, turn::TurnOutcome::Ok | turn::TurnOutcome::Compacted) {
|
||||
ack_turn(socket).await;
|
||||
}
|
||||
if matches!(outcome, turn::TurnOutcome::RateLimited) {
|
||||
let secs = turn::rate_limit_sleep_secs();
|
||||
bus.emit_status("rate_limited");
|
||||
bus.emit(LiveEvent::Note {
|
||||
text: format!("API rate-limited — sleeping {secs}s before retry"),
|
||||
});
|
||||
tracing::warn!(sleep_secs = secs, "rate-limited; parking");
|
||||
tokio::time::sleep(Duration::from_secs(secs)).await;
|
||||
requeue_inflight(socket).await;
|
||||
bus.emit_status("online");
|
||||
}
|
||||
if let Some(stats) = stats {
|
||||
let ended_at = serve_common::now_unix();
|
||||
let duration_ms =
|
||||
i64::try_from(started_instant.elapsed().as_millis()).unwrap_or(i64::MAX);
|
||||
let (open_threads, open_reminders) = fetch_manager_post_turn_counts(socket).await;
|
||||
let row = serve_common::build_row(
|
||||
started_at,
|
||||
ended_at,
|
||||
duration_ms,
|
||||
model_at_start,
|
||||
from.clone(),
|
||||
&outcome,
|
||||
bus,
|
||||
open_threads,
|
||||
open_reminders,
|
||||
);
|
||||
stats.record(&row);
|
||||
}
|
||||
let pending = inbox_unread(socket).await;
|
||||
if pending > 0 {
|
||||
tracing::info!(%pending, "pending messages after turn; fetching next");
|
||||
}
|
||||
}
|
||||
|
||||
/// Best-effort: tell the broker every message popped during the turn
|
||||
/// is now handled. Mirror of `hive-ag3nt::ack_turn` on the manager
|
||||
|
|
|
|||
|
|
@ -422,8 +422,7 @@ fn format_state_change_notification(
|
|||
&& subject
|
||||
.as_ref()
|
||||
.and_then(|s| s["requested_reviewers"].as_array())
|
||||
.map(|arr| arr.iter().any(|r| r["login"].as_str() == Some(own_login)))
|
||||
.unwrap_or(false);
|
||||
.is_some_and(|arr| arr.iter().any(|r| r["login"].as_str() == Some(own_login)));
|
||||
let kind = if is_review_request {
|
||||
format!("review requested{num}{repo}")
|
||||
} else {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue