Watch
0
0
Fork
You've already forked hyperhive
0

hive-c0re: remove the approvals subsystem

Nothing submits an approval since scheduled prompts left hive-c0re, so the
queue only acted on rows written before that.

- hive-c0re: the approvals store and its migrations, approve/deny/
  resolve actions, the ResolveApproval tail node (and meta_update's
  approval_id), POST /api/approve and /api/deny, the approval views on
  /api/state, the approval_resolved dashboard event, the orphan GC,
  destroy/purge failing pending rows, and the approval rows in
  get_loose_ends. DestroyBookkeeping loses the purge flag, which only
  worded the approval-failure note.
- The CancelLooseEnd request goes: its only hive-c0re arm was the
  approval one. cancel_loose_end handles reminder and todo in the harness.
- Wire types: hive_sh4re::approvals (ReminderStats moves to inbox),
  LooseEnd::Approval, CancelLooseEndKind, HelperEvent::ApprovalResolved,
  ToolGroup::Approvals, HostRequest::{Pending,Approve,Deny} and
  HostResponse.approvals. HOST_SOCK_VERSION goes to 2 for the removed
  field.
- hivectl loses the approvals verbs; docs/tools/hivectl-cli.md is
  regenerated.
- The system prompt drops approval_resolved.
- The approvals table stays in broker.sqlite, unread, the same way the
  operator_questions table was left when its store was removed.
- An `approvals` entry still in tool-groups.json is skipped with a warning
  by ToolGroup::parse_list, like any unknown group.
- docs/agent-lifecycle/approvals.md is renamed to config-changes.md so the
  code's doc pointers resolve; its content follows in the docs commit.
This commit is contained in:
atlas 2026-10-04 23:55:03 +02:00
commit 58ef299c3c

View file

@ -1,77 +1,34 @@
//! Loose-ends aggregator. Walks the `approvals` table once per call and
//! assembles a `Vec<LooseEnd>` for a single agent (`for_agent`) — always
//! the caller's own. `Request::GetLooseEnds` from either the agent or
//! manager socket lands here so the age-seconds derivation stays in one
//! place. Reminders are agent-local (in-container
//! `hive-agent::reminders` store) and never sourced from here. The
//! `ask`/`answer` MCP tools, their wire protocol (`hive-c0re::questions`,
//! `stores::operator_questions`), and the operator dashboard's questions
//! pane have all been removed entirely — this file never had a
//! `Question` loose-end path to begin with (it only ever emitted
//! `PendingMessages`/`Approval`), so nothing here changed shape when
//! that removal landed.
//! Loose-ends aggregator. Assembles a `Vec<LooseEnd>` for a single agent
//! (`for_agent`) — always the caller's own. `Request::GetLooseEnds` from
//! either the agent or manager socket lands here. Reminders and todos are
//! agent-local (in-container `hive-agent` stores) and never sourced from
//! here.
//!
//! 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 index (`idx_approvals_pending`)
//! that the dashboard uses, so the bottleneck would be json
//! (de)serialisation, not the read.
//! mutation events.
use anyhow::Result;
use chrono::Utc;
use hive_sh4re::inbox::{LooseEnd, saturating_age};
use hive_sh4re::inbox::LooseEnd;
use crate::coordinator::Coordinator;
/// Open threads pending against `agent`:
/// - undelivered inbox messages this agent still owes itself a `recv`
/// for (only when the count is non-zero);
/// - 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.
///
/// 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).
/// Open threads pending against `agent`: undelivered inbox messages this
/// agent still owes itself a `recv` for, only when the count is non-zero.
///
/// # Errors
///
/// Propagates errors from `count_pending` and the pending-approval
/// sqlite query.
/// Propagates errors from `count_pending`.
pub fn for_agent(coord: &Coordinator, agent: &str) -> Result<Vec<LooseEnd>> {
let now = Utc::now().timestamp();
let mut out = Vec::new();
// Undelivered inbox messages this agent still owes itself a `recv`
// for. Surfaced first (most actionable) and only when non-zero so a
// clean inbox doesn't add noise. The wake-message that drove the
// current turn is already delivered, so this counts only what's
// genuinely still queued.
// Only emitted when non-zero so a clean inbox doesn't add noise. The
// wake-message that drove the current turn is already delivered, so
// this counts only what's genuinely still queued.
let pending_messages = coord.broker.count_pending(agent)?;
if pending_messages > 0 {
out.push(LooseEnd::PendingMessages {
count: pending_messages,
});
}
// Show each pending approval to the agent that submitted it. The
// submitter column is NULL for rows predating it; those count as
// operator-initiated (no agent tracking predates the column).
for a in coord.approvals.pending()? {
let submitter = coord
.approvals
.submitter_of(a.id)?
.unwrap_or_else(|| "operator".to_owned());
if submitter != agent {
continue;
}
out.push(LooseEnd::Approval {
id: a.id,
agent: a.agent.to_string(),
commit_ref: a.commit_ref,
description: a.description,
age_seconds: saturating_age(now, a.requested_at.timestamp()),
});
}
Ok(out)
}