wire reminder ops into the in-agent socket dispatch (#2635 inc 1)

This commit is contained in:
damocles 2026-07-22 21:01:24 +02:00 committed by mara
commit ecd9030305
6 changed files with 178 additions and 45 deletions

View file

@ -216,6 +216,11 @@ fn graceful_stop_message() -> hive_sh4re::DeliveredMessage {
/// enum so `serve_loop` can pattern-match without seeing it directly.
enum RecvOutcome {
/// Long-poll returned at least one message; first one is detached.
/// Also reused for a fired reminder: `reminder_timer` pushes a *real*
/// `DeliveredMessage` (unlike `LocalTodo`'s synthetic hint) since the
/// body/id is per-row data the producer already resolved, so the
/// select arm wraps it straight into this variant — no dedicated
/// `LocalReminder` variant needed.
Message(hive_sh4re::DeliveredMessage),
/// Long-poll timed out cleanly (empty `Messages` response). Caller
/// sleeps then retries.
@ -234,11 +239,6 @@ enum RecvOutcome {
LocalTodo,
}
/// Reminder fires are pushed as a *real* `DeliveredMessage` (unlike
/// `LocalTodo`'s synthetic hint) since the body/id is per-row data the
/// producer (`reminder_timer`) already resolved — so the select arm
/// wraps it straight into `RecvOutcome::Message`, no dedicated variant.
/// Wire surface abstraction. `AgentSurface` is the only impl — the trait
/// exists to keep the turn loop generic and testable. Every function that
/// talks to the broker goes through this so there are zero hard-coded
@ -460,33 +460,15 @@ async fn serve_main<S: Surface>(socket: &Path, poll_ms: u64) -> Result<()> {
tracing::error!(error = %e, "web_ui::serve exited with error");
}
});
// In-agent todo socket (loose-ends v2): the harness owns the todo store
// locally and serves the in-container producers on `HIVE_AGENT_SOCKET`.
// A new/changed upsert fires `todo_wake` so the serve loop drives a turn
// directly — no broker round-trip, no marker files. Best-effort: if the
// store can't open, the socket just isn't served.
let todo_wake = Arc::new(tokio::sync::Notify::new());
match todos::Todos::open(&paths::todos_db()) {
Ok(store) => {
let store = Arc::new(store);
let wake = todo_wake.clone();
tokio::spawn(async move {
if let Err(e) = todo_server::run(store, wake).await {
tracing::error!(error = %e, "in-agent todo socket exited with error");
}
});
}
Err(e) => {
tracing::error!(error = ?e, "open todos db failed — in-agent todo socket disabled");
}
}
// Harness-local reminders (#2635 increment 1): a due reminder fires
// straight into an mpsc channel the serve loop races against the
// broker long-poll (unlike todos, a fire carries real per-row data,
// so a bare `Notify` doesn't fit — see `reminder_timer` docs).
// Best-effort like the todo store above: `reminder_timer::run` parks
// forever (never sends) instead of exiting when the store can't
// open, so the channel never observes a "closed" state.
// Best-effort: `reminder_timer::run` parks forever (never sends)
// instead of exiting when the store can't open, so the channel
// never observes a "closed" state. Opened before the todo socket
// below so the same `Arc` can be handed to its request dispatch
// (reminder ops share the todo socket/listener).
let (reminder_tx, reminder_rx) = tokio::sync::mpsc::unbounded_channel();
let reminder_store = match reminders::Reminders::open(&paths::reminders_db()) {
Ok(store) => Some(Arc::new(store)),
@ -495,7 +477,32 @@ async fn serve_main<S: Surface>(socket: &Path, poll_ms: u64) -> Result<()> {
None
}
};
tokio::spawn(reminder_timer::run(reminder_store, reminder_tx));
tokio::spawn(reminder_timer::run(reminder_store.clone(), reminder_tx));
// In-agent todo socket (loose-ends v2 + #2635 reminders): the harness
// owns the todo + reminder stores locally and serves the
// in-container producers on `HIVE_AGENT_SOCKET`. A new/changed todo
// upsert fires `todo_wake` so the serve loop drives a turn directly —
// no broker round-trip, no marker files. Best-effort: if the todos
// store can't open, the whole socket isn't served (reminder ops ride
// along on the same listener, so they're gated on the same store —
// acceptable since a from-scratch harness boot either has a writable
// harness dir or doesn't).
let todo_wake = Arc::new(tokio::sync::Notify::new());
match todos::Todos::open(&paths::todos_db()) {
Ok(store) => {
let store = Arc::new(store);
let wake = todo_wake.clone();
let reminders = reminder_store.clone();
tokio::spawn(async move {
if let Err(e) = todo_server::run(store, wake, reminders).await {
tracing::error!(error = %e, "in-agent todo socket exited with error");
}
});
}
Err(e) => {
tracing::error!(error = ?e, "open todos db failed — in-agent todo socket disabled");
}
}
if matches!(initial, LoginState::NeedsLogin) {
login::wait_for_login(&claude_dir, login_state.clone(), &bus, poll_ms).await;
} else {