feat(#2635): wire harness-local questions mirror (inc2 pt2)

This commit is contained in:
damocles 2026-07-23 21:10:51 +02:00 committed by mara
commit e5ef5a72be
8 changed files with 465 additions and 191 deletions

View file

@ -1,17 +1,22 @@
//! Loose-ends aggregator. Walks the `approvals` + `operator_questions`
//! tables once per call and assembles a `Vec<LooseEnd>` for either
//! a single agent (`for_agent`) or the whole hive (`hive_wide`).
//! `Request::GetLooseEnds` from either the agent or manager socket
//! lands here so the routing logic + age-seconds derivation stay in
//! one place. Reminders are agent-local (in-container store) and no
//! longer sourced from here.
//! Loose-ends aggregator. Walks the `approvals` table once per call and
//! assembles a `Vec<LooseEnd>` for either a single agent (`for_agent`) or
//! the whole hive (`hive_wide`). `Request::GetLooseEnds` from either the
//! agent or manager socket lands here so the routing logic + age-seconds
//! derivation stay in one place. Reminders AND questions are agent-local
//! (in-container stores, `hive-agent::reminders` / `hive-agent::questions`)
//! and no longer sourced from here (loose-ends-v2's questions phase) —
//! c0re remains the `Ask`/`Answer` routing + delivery rendezvous
//! (`coord.questions`), it just isn't asked for the *pending-view*
//! rendering anymore. The
//! operator dashboard's questions pane is unaffected: it reads
//! `coord.questions.pending_all()` directly (`dashboard/state_snapshot.rs`),
//! independent of this module.
//!
//! Call frequency is low (an agent doing self-introspection between
//! turns), so the sweep happens fresh every time — no caching, no
//! mutation events. If the sweep ever shows up in a profile, the
//! sqlite queries already filter on the same indexes
//! (`idx_approvals_pending` + `idx_operator_questions_pending`) that
//! the dashboard uses, so the bottleneck would be json
//! mutation events. If the sweep ever shows up in a profile, the sqlite
//! queries already filter on the same index (`idx_approvals_pending`)
//! that the dashboard uses, so the bottleneck would be json
//! (de)serialisation, not the read.
use anyhow::Result;
@ -26,19 +31,16 @@ use hive_sh4re::wire_time::now_unix;
/// - pending approvals where this agent is the submitter (a parent
/// agent with the `approvals` group submits for its children; the
/// root submits for top-level agents). Legacy rows with no recorded
/// submitter count as the root's;
/// - unanswered questions where `agent` is the asker (waiting on
/// someone) OR the target (owes a reply).
/// submitter count as the root's.
///
/// Ordered `pending_messages` (when non-zero) → approvals → questions
/// within the returned vector. Within each kind, source-of-truth
/// ordering (sqlite's `pending()` queries return newest-first within
/// their indexes).
/// Ordered `pending_messages` (when non-zero) → approvals within the
/// returned vector. Within each kind, source-of-truth ordering (sqlite's
/// `pending()` query returns newest-first within its index).
///
/// # Errors
///
/// Propagates errors from `count_pending` and the pending-approval /
/// question sqlite queries.
/// Propagates errors from `count_pending` and the pending-approval
/// sqlite query.
pub fn for_agent(coord: &Coordinator, agent: &str) -> Result<Vec<LooseEnd>> {
let now = now_unix();
let mut out = Vec::new();
@ -72,26 +74,12 @@ pub fn for_agent(coord: &Coordinator, agent: &str) -> Result<Vec<LooseEnd>> {
age_seconds: saturating_age(now, a.requested_at.timestamp()),
});
}
for q in coord.questions.pending_all()? {
let role_match = q.asker == agent || q.target.as_deref() == Some(agent);
if !role_match {
continue;
}
out.push(LooseEnd::Question {
id: q.id,
asker: q.asker,
target: q.target,
question: q.question,
age_seconds: saturating_age(now, q.asked_at.timestamp()),
});
}
Ok(out)
}
/// Hive-wide loose-ends view: EVERY pending approval + EVERY
/// unanswered question. Manager surface only; sub-agents can't see
/// each other's threads via the agent surface (`for_agent` filters by
/// name).
/// Hive-wide loose-ends view: EVERY pending approval. Manager surface
/// only; sub-agents can't see each other's threads via the agent surface
/// (`for_agent` filters by name).
pub fn hive_wide(coord: &Coordinator) -> Result<Vec<LooseEnd>> {
let now = now_unix();
let mut out = Vec::new();
@ -104,15 +92,6 @@ pub fn hive_wide(coord: &Coordinator) -> Result<Vec<LooseEnd>> {
age_seconds: saturating_age(now, a.requested_at.timestamp()),
});
}
for q in coord.questions.pending_all()? {
out.push(LooseEnd::Question {
id: q.id,
asker: q.asker,
target: q.target,
question: q.question,
age_seconds: saturating_age(now, q.asked_at.timestamp()),
});
}
Ok(out)
}