feat(#1990): surface pending recv() message count in get_loose_ends

This commit is contained in:
damocles 2026-06-26 00:25:28 +02:00 committed by mara
commit 49db9d6e1c
5 changed files with 44 additions and 0 deletions

View file

@ -223,6 +223,18 @@ Per-variant fields:
- `Reminder { id, owner, message, due_at, age_seconds }`
`due_at` is the absolute unix timestamp the scheduler is
targeting; clients compute time-until-fire as `due_at - now`.
- `PendingMessages { count }` — undelivered inbox messages the
agent still owes itself a `recv` for. Informational + not
cancellable (drain with `recv`); only emitted when `count > 0`,
and surfaced first in the agent-flavour list as the most
actionable signal. Counted host-side from the broker
(`count_pending`), so it reflects what's genuinely still queued —
the wake-message that drove the current turn is already delivered
and not counted.
- `UnreadMatrix { rooms, summary }` — unread matrix notifications.
Informational + not cancellable (clear with `mark_read`). Unlike
the others this is injected by the in-container harness, not
hive-c0re, because the matrix daemon lives inside the agent.
`age_seconds` saturates at zero on any clock anomaly (back-step,
unsynchronised wall clock, etc.) so the bulleted list never

View file

@ -891,6 +891,12 @@ window.marked = marked;
el('span', { class: 'inbox-ts' }, 'scheduled ' + fmtAge(t.age_seconds || 0) + ' ago'),
el('div', { class: 'inbox-body' }, t.message || ''),
);
} else if (t.kind === 'pending_messages') {
li.append(
el('span', { class: 'inbox-from' }, '✉ inbox'), ' ',
el('span', { class: 'inbox-sep' }, (t.count || 0) + ' pending message(s)'), ' ',
el('span', { class: 'inbox-ts' }, 'drain with recv'),
);
} else {
li.append(el('span', { class: 'inbox-body' }, JSON.stringify(t)));
}

View file

@ -298,6 +298,12 @@ fn render_loose_ends(loose_ends: &[hive_sh4re::LooseEnd]) -> String {
"- reminder #{id} ({owner}, scheduled {age_seconds}s ago, due_at={due_at}): {message}"
);
}
hive_sh4re::LooseEnd::PendingMessages { count } => {
let _ = writeln!(
out,
"- {count} pending inbox message(s) — drain with recv (recv(max: {count}) to batch)"
);
}
hive_sh4re::LooseEnd::UnreadMatrix { rooms, summary } => {
let _ = write!(out, "- unread matrix messages in {rooms} room(s)");
if summary.is_empty() {

View file

@ -35,6 +35,17 @@ use crate::coordinator::Coordinator;
pub fn for_agent(coord: &Coordinator, agent: &str) -> Result<Vec<LooseEnd>> {
let now = now_unix();
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.
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
// the root agent's.

View file

@ -386,6 +386,15 @@ pub enum LooseEnd {
due_at: i64,
age_seconds: u64,
},
/// Undelivered inbox messages waiting to be `recv`'d by this agent.
/// Not cancellable — drain them with `recv`. Surfaced so an agent
/// doing a between-turns `get_loose_ends` sweep sees it still owes
/// itself a `recv` without having to poll the inbox separately. Only
/// emitted when `count > 0`.
PendingMessages {
/// Number of undelivered messages queued for this agent.
count: u64,
},
/// Unread matrix notifications in one or more rooms. Not cancellable —
/// use `mark_read` via the matrix MCP to clear. Injected by the
/// in-container harness (not hive-c0re) because the matrix daemon