harness: flip into needs_login on 401 mid-turn (closes #419)
This commit is contained in:
parent
599a71254a
commit
799804e3d1
6 changed files with 164 additions and 22 deletions
|
|
@ -99,6 +99,7 @@ async fn main() -> Result<()> {
|
|||
&cli.socket,
|
||||
Duration::from_millis(poll_ms),
|
||||
login_state,
|
||||
claude_dir,
|
||||
bus,
|
||||
stats,
|
||||
&files,
|
||||
|
|
@ -116,6 +117,7 @@ async fn main() -> Result<()> {
|
|||
&cli.socket,
|
||||
Duration::from_millis(poll_ms),
|
||||
login_state,
|
||||
claude_dir,
|
||||
bus,
|
||||
stats,
|
||||
&files,
|
||||
|
|
@ -153,7 +155,8 @@ async fn main() -> Result<()> {
|
|||
async fn serve(
|
||||
socket: &Path,
|
||||
interval: Duration,
|
||||
_login_state: Arc<Mutex<LoginState>>,
|
||||
login_state: Arc<Mutex<LoginState>>,
|
||||
claude_dir: std::path::PathBuf,
|
||||
bus: Bus,
|
||||
stats: Option<TurnStats>,
|
||||
files: &turn::TurnFiles,
|
||||
|
|
@ -178,7 +181,23 @@ async fn serve(
|
|||
match recv {
|
||||
Ok(AgentResponse::Messages { messages }) if !messages.is_empty() => {
|
||||
let first = messages.into_iter().next().expect("checked non-empty");
|
||||
handle_agent_turn(socket, &bus, stats.as_ref(), files, &turn_lock, label, first).await;
|
||||
let auth_failed =
|
||||
handle_agent_turn(socket, &bus, stats.as_ref(), files, &turn_lock, label, first)
|
||||
.await;
|
||||
if auth_failed {
|
||||
// Park: flip LoginState + wait for the operator's
|
||||
// re-auth to repopulate claude_dir. wait_for_login
|
||||
// emits `online` on resume, which clears the
|
||||
// needs_login sentinel.
|
||||
*login_state.lock().unwrap() = LoginState::NeedsLogin;
|
||||
turn::wait_for_login(
|
||||
&claude_dir,
|
||||
login_state.clone(),
|
||||
&bus,
|
||||
u64::try_from(interval.as_millis()).unwrap_or(2000),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
Ok(AgentResponse::Messages { .. }) => {
|
||||
// Idle: empty list = nothing pending. Brief sleep
|
||||
|
|
@ -208,7 +227,9 @@ async fn serve(
|
|||
}
|
||||
}
|
||||
|
||||
/// Drive one turn for a received agent-inbox message.
|
||||
/// Drive one turn for a received agent-inbox message. Returns `true`
|
||||
/// when the turn ended with `AuthFailed` so the caller knows to park
|
||||
/// in `wait_for_login`.
|
||||
async fn handle_agent_turn(
|
||||
socket: &Path,
|
||||
bus: &Bus,
|
||||
|
|
@ -217,7 +238,7 @@ async fn handle_agent_turn(
|
|||
turn_lock: &TurnLock,
|
||||
label: &str,
|
||||
first: hive_sh4re::DeliveredMessage,
|
||||
) {
|
||||
) -> bool {
|
||||
let from = first.from;
|
||||
let body = first.body;
|
||||
let redelivered = first.redelivered;
|
||||
|
|
@ -251,6 +272,19 @@ async fn handle_agent_turn(
|
|||
requeue_inflight(socket).await;
|
||||
bus.emit_status("online");
|
||||
}
|
||||
// 401: flip into needs_login + requeue the message that triggered
|
||||
// the turn so it survives the re-auth. The serve loop's outer
|
||||
// login-state watcher parks until the operator's `/login` flow
|
||||
// completes; once it does, the requeued message replays the turn
|
||||
// (closes #419).
|
||||
if matches!(outcome, turn::TurnOutcome::AuthFailed) {
|
||||
bus.emit_status("needs_login_idle");
|
||||
bus.emit(LiveEvent::Note {
|
||||
text: "API 401 — waiting for re-login via web UI".into(),
|
||||
});
|
||||
tracing::warn!("auth-failed; parking until re-login");
|
||||
requeue_inflight(socket).await;
|
||||
}
|
||||
// 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;
|
||||
|
|
@ -280,6 +314,7 @@ async fn handle_agent_turn(
|
|||
// `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;
|
||||
matches!(outcome, turn::TurnOutcome::AuthFailed)
|
||||
}
|
||||
|
||||
// Per-turn user prompt: the role/tools/etc. is in the system prompt
|
||||
|
|
|
|||
|
|
@ -88,6 +88,8 @@ async fn main() -> Result<()> {
|
|||
serve(
|
||||
&cli.socket,
|
||||
Duration::from_millis(poll_ms),
|
||||
login_state,
|
||||
claude_dir,
|
||||
bus,
|
||||
stats,
|
||||
&files,
|
||||
|
|
@ -96,10 +98,12 @@ async fn main() -> Result<()> {
|
|||
.await
|
||||
}
|
||||
LoginState::NeedsLogin => {
|
||||
turn::wait_for_login(&claude_dir, login_state, &bus, poll_ms).await;
|
||||
turn::wait_for_login(&claude_dir, login_state.clone(), &bus, poll_ms).await;
|
||||
serve(
|
||||
&cli.socket,
|
||||
Duration::from_millis(poll_ms),
|
||||
login_state,
|
||||
claude_dir,
|
||||
bus,
|
||||
stats,
|
||||
&files,
|
||||
|
|
@ -113,9 +117,12 @@ async fn main() -> Result<()> {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn serve(
|
||||
socket: &Path,
|
||||
interval: Duration,
|
||||
login_state: Arc<Mutex<LoginState>>,
|
||||
claude_dir: std::path::PathBuf,
|
||||
bus: Bus,
|
||||
stats: Option<TurnStats>,
|
||||
files: &turn::TurnFiles,
|
||||
|
|
@ -144,7 +151,19 @@ async fn serve(
|
|||
match recv {
|
||||
Ok(ManagerResponse::Messages { messages }) if !messages.is_empty() => {
|
||||
let first = messages.into_iter().next().expect("checked non-empty");
|
||||
handle_manager_turn(socket, &bus, stats.as_ref(), files, &turn_lock, first).await;
|
||||
let auth_failed =
|
||||
handle_manager_turn(socket, &bus, stats.as_ref(), files, &turn_lock, first)
|
||||
.await;
|
||||
if auth_failed {
|
||||
*login_state.lock().unwrap() = LoginState::NeedsLogin;
|
||||
turn::wait_for_login(
|
||||
&claude_dir,
|
||||
login_state.clone(),
|
||||
&bus,
|
||||
u64::try_from(interval.as_millis()).unwrap_or(2000),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
Ok(ManagerResponse::Messages { .. }) => {
|
||||
// Idle: empty list = nothing pending. Brief sleep
|
||||
|
|
@ -176,6 +195,8 @@ 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.
|
||||
/// Returns `true` when the turn ended with `AuthFailed` so the caller
|
||||
/// can park in `wait_for_login`.
|
||||
async fn handle_manager_turn(
|
||||
socket: &Path,
|
||||
bus: &Bus,
|
||||
|
|
@ -183,7 +204,7 @@ async fn handle_manager_turn(
|
|||
files: &turn::TurnFiles,
|
||||
turn_lock: &TurnLock,
|
||||
first: hive_sh4re::DeliveredMessage,
|
||||
) {
|
||||
) -> bool {
|
||||
let from = first.from;
|
||||
let body = first.body;
|
||||
let redelivered = first.redelivered;
|
||||
|
|
@ -229,6 +250,14 @@ async fn handle_manager_turn(
|
|||
requeue_inflight(socket).await;
|
||||
bus.emit_status("online");
|
||||
}
|
||||
if matches!(outcome, turn::TurnOutcome::AuthFailed) {
|
||||
bus.emit_status("needs_login_idle");
|
||||
bus.emit(LiveEvent::Note {
|
||||
text: "API 401 — waiting for re-login via web UI".into(),
|
||||
});
|
||||
tracing::warn!("auth-failed; parking until re-login");
|
||||
requeue_inflight(socket).await;
|
||||
}
|
||||
if let Some(stats) = stats {
|
||||
let ended_at = serve_common::now_unix();
|
||||
let duration_ms =
|
||||
|
|
@ -251,6 +280,7 @@ async fn handle_manager_turn(
|
|||
if pending > 0 {
|
||||
tracing::info!(%pending, "pending messages after turn; fetching next");
|
||||
}
|
||||
matches!(outcome, turn::TurnOutcome::AuthFailed)
|
||||
}
|
||||
|
||||
/// Best-effort: tell the broker every message popped during the turn
|
||||
|
|
|
|||
Loading…
Reference in a new issue