Watch
0
0
Fork
You've already forked hyperhive
0

hive-agent: a logged-in agent polls the swarm login, and logs out when it is deleted

wait_for_login read the store leaf only while the agent waited for a login.
A logged-in agent now reads it every 5 minutes, and a paused one once before
it resumes, so a login made or deleted in the swarm UI reaches an agent that
never failed. A newer version is installed; a store that answers nothing
stored after a version was installed means the stored login was deleted, and
the agent is logged out as /api/logout does it, with the recorded version
forgotten so the next login installs. A read that fails changes nothing.
This commit is contained in:
iris 2026-11-09 22:08:05 +01:00 • committed by mara
commit a2c9e9a8d4

View file

@ -6,9 +6,10 @@ use axum::{
response::{IntoResponse, Response},
};
use crate::events::Bus;
use crate::login::LoginState;
use super::AppState;
use super::{AppState, LoginStateCell};
/// Operator-driven `/logout`: SIGINT claude, delete the credential
/// files (via [`crate::login::clear_session`]), flip `LoginState::NeedsLogin`.
@ -20,6 +21,13 @@ use super::AppState;
/// (the `/api/logout` bullet) for the three-step rationale +
/// preservation invariants.
pub(super) async fn post_logout(State(state): State<AppState>) -> Response {
let said = force_logout(&state.login, &state.bus, "operator: /logout").await;
(axum::http::StatusCode::OK, format!("ok: {said}")).into_response()
}
/// The three steps of a logout, shared by `/api/logout` and the swarm login
/// watch: `by` heads the Note. Returns what was wiped, for the response.
pub(crate) async fn force_logout(login: &LoginStateCell, bus: &Bus, by: &str) -> String {
// Step 1: SIGINT claude (best-effort, matches `post_cancel_turn`).
let _ = super::sigint_claude();
// Step 2: delete OAuth credential files only — login::clear_session owns
@ -34,19 +42,13 @@ pub(super) async fn post_logout(State(state): State<AppState>) -> Response {
};
// Step 3: flip LoginState + emit Note. Turn loop sees the flip on
// its next iteration and parks into wait_for_login.
*state.login.lock().unwrap() = LoginState::NeedsLogin;
state.bus.emit(crate::events::LiveEvent::Note {
text: format!(
"operator: /logout — {wipe_summary} in {}{warn_suffix}",
dir.display()
),
*login.lock().unwrap() = LoginState::NeedsLogin;
let said = format!("{wipe_summary} in {}{warn_suffix}", dir.display());
bus.emit(crate::events::LiveEvent::Note {
text: format!("{by} — {said}"),
});
state.bus.emit_status("needs_login_idle");
(
axum::http::StatusCode::OK,
format!("ok: {wipe_summary} in {}{warn_suffix}", dir.display()),
)
.into_response()
bus.emit_status("needs_login_idle");
said
}
/// Human-readable summary of a [`crate::login::ClearedSession`] outcome for