gate stale todo wakes on an emptiness check (#2678)
This commit is contained in:
parent
dfadacd45f
commit
001ea38ea4
2 changed files with 65 additions and 3 deletions
|
|
@ -500,22 +500,32 @@ async fn serve_main<S: Surface>(socket: &Path, poll_ms: u64) -> Result<()> {
|
|||
// 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::state_db()) {
|
||||
// Kept alongside `todo_wake` so the serve loop's `LocalTodo` arm can
|
||||
// gate a wake on `has_any()` before spawning a turn — see its doc
|
||||
// comment (the phantom-todo-wake issue: a burst of same-turn upserts
|
||||
// can arm a second `Notify` permit that outlives the turn that
|
||||
// already drained its payload).
|
||||
let todos_store: Option<Arc<todos::Todos>> = match todos::Todos::open(&paths::state_db()) {
|
||||
Ok(store) => {
|
||||
let store = Arc::new(store);
|
||||
let wake = todo_wake.clone();
|
||||
let reminders = reminder_store.clone();
|
||||
let bus_for_socket = bus.clone();
|
||||
let store_for_socket = store.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = todo_server::run(store, wake, reminders, bus_for_socket).await {
|
||||
if let Err(e) =
|
||||
todo_server::run(store_for_socket, wake, reminders, bus_for_socket).await
|
||||
{
|
||||
tracing::error!(error = %e, "in-agent todo socket exited with error");
|
||||
}
|
||||
});
|
||||
Some(store)
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(error = ?e, "open todos db failed — in-agent todo socket disabled");
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
if matches!(initial, LoginState::NeedsLogin) {
|
||||
login::wait_for_login(&claude_dir, login_state.clone(), &bus, poll_ms).await;
|
||||
} else {
|
||||
|
|
@ -533,6 +543,7 @@ async fn serve_main<S: Surface>(socket: &Path, poll_ms: u64) -> Result<()> {
|
|||
stats,
|
||||
&files,
|
||||
todo_wake,
|
||||
todos_store,
|
||||
reminder_rx,
|
||||
)
|
||||
.await
|
||||
|
|
@ -556,6 +567,7 @@ async fn serve_loop<S: Surface>(
|
|||
stats: Option<TurnStats>,
|
||||
files: &turn::TurnFiles,
|
||||
todo_wake: Arc<tokio::sync::Notify>,
|
||||
todos_store: Option<Arc<todos::Todos>>,
|
||||
mut reminder_rx: tokio::sync::mpsc::UnboundedReceiver<hive_sh4re::DeliveredMessage>,
|
||||
) -> Result<()> {
|
||||
tracing::info!(socket = %socket.display(), "harness serve");
|
||||
|
|
@ -586,6 +598,22 @@ async fn serve_loop<S: Surface>(
|
|||
} {
|
||||
RecvOutcome::Message(first) => first,
|
||||
RecvOutcome::LocalTodo => {
|
||||
// Gate on `has_any()` before spawning a turn: a burst of
|
||||
// same-turn upserts can arm a second `Notify` permit that
|
||||
// outlives the turn which already drained its payload
|
||||
// (the phantom-todo-wake issue) — `notify_one` doesn't
|
||||
// coalesce once the first permit's been consumed, so the surplus wake
|
||||
// fires the instant the loop is back here even though
|
||||
// there's nothing left to show. Fail open (drive a turn
|
||||
// anyway) on a `has_any` error so a flaky sqlite read
|
||||
// never silently swallows a real wake.
|
||||
let has_any = todos_store
|
||||
.as_ref()
|
||||
.is_none_or(|store| store.has_any().unwrap_or(true));
|
||||
if !has_any {
|
||||
tracing::debug!("todo wake fired against an empty store — stale, skipping");
|
||||
continue;
|
||||
}
|
||||
tracing::debug!("todo wake consumed, sending synthetic todo message");
|
||||
synthetic_todo_message()
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue