feat(#2626): migrate forge_notify to the in-agent todo socket (per-thread todos, not wakes)

This commit is contained in:
damocles 2026-07-21 00:47:07 +02:00
commit 1e1ca50da3
2 changed files with 36 additions and 21 deletions

View file

@ -64,6 +64,8 @@ const NEW_ITEM_TOLERANCE_SECS: i64 = 120;
/// configured. Otherwise loops forever, polling every
/// `POLL_INTERVAL_SECS` seconds. Errors are never fatal.
///
/// `socket` is the harness's in-agent todo socket (`HIVE_AGENT_SOCKET`):
/// each forge notification is pushed as an `upsert_todo`, not a direct wake.
pub async fn run(socket: PathBuf) {
let forge_url = match std::env::var("HIVE_FORGE_URL") {
Ok(u) if !u.is_empty() => u,
@ -1064,33 +1066,40 @@ async fn poll_once(
continue;
};
let req = hive_core_agent_sock::Request::Wake {
from: "forge".to_owned(),
body,
// Upsert a *todo* (loose-ends v2) on the harness's in-agent socket,
// keyed by the forge thread id, instead of firing a direct wake. A
// new/changed summary makes the harness signal its turn loop; the
// agent clears the todo (`mark_todo_done`) once it has handled the
// thread. Re-scanning the same thread is an idempotent no-op.
let req = hive_agent_sock::Request::UpsertTodo {
subsystem: "forge".to_owned(),
key: Some(id.to_string()),
summary: body,
source: None,
};
let deliver_result =
crate::client::request::<_, hive_core_agent_sock::Response>(socket, &req)
.await
.map(|_| ());
let deliver_result = crate::client::request::<_, hive_agent_sock::Response>(socket, &req)
.await
.map(|_| ());
match deliver_result {
Ok(()) => {
debug!(%id, "forge_notify: delivered");
// Mark the thread read on forge immediately after a
// successful broker delivery. The broker inbox is the
// durable work queue now (each row has its own ack
// lifecycle), so the forge unread flag no longer needs to
// track agent processing — clearing it on delivery keeps
// forge's unread set tiny by construction, so a container
// rebuild re-scan finds nothing stale to re-deliver. The
// in-memory `delivered` entry below is only a within-process
// guard so a transient mark-read failure doesn't re-fire the
// wake next tick; it is deliberately NOT persisted — forge's
// own read-state is the cross-rebuild source of truth.
debug!(%id, "forge_notify: todo upserted");
// Mark the thread read on forge immediately after upserting
// the todo. The todo is the durable work item now (it stays
// in `get_loose_ends` until the agent marks it done), so the
// forge unread flag no longer needs to track agent
// processing — clearing it keeps forge's unread set tiny by
// construction, so a container rebuild re-scan finds nothing
// stale (and idempotent re-upserts wouldn't re-wake anyway).
// The in-memory `delivered` entry below is only a
// within-process guard so a transient mark-read failure
// doesn't re-upsert next tick; it is deliberately NOT
// persisted — forge's own read-state is the cross-rebuild
// source of truth.
mark_read(forge, id).await;
delivered.insert(id, updated_at);
}
Err(e) => {
warn!(%id, error = ?e, "forge_notify: deliver failed — leaving unread");
warn!(%id, error = ?e, "forge_notify: todo upsert failed — leaving unread");
}
}
}