diff --git a/CLAUDE.md b/CLAUDE.md index b135dea4..bba3919f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -53,12 +53,11 @@ hive-c0re/ host daemon + CLI (one binary, subcommand-dispatched) transient/sockets) + tombstone enumeration + kick_agent + notify_agent (helper-event push) + last_containers cache + rescan_and_emit diff helper - src/loose_ends.rs loose-ends aggregator (pending approvals + - unanswered questions + pending reminders) — - for_agent (filtered) and hive_wide (manager - surface). Backs AgentRequest::GetLooseEnds + - ManagerRequest::GetLooseEnds (the - get_loose_ends MCP tool). + src/open_threads.rs loose-ends aggregator (pending approvals + + unanswered questions) — for_agent (filtered) and + hive_wide (manager surface). Backs + AgentRequest::GetOpenThreads + ManagerRequest:: + GetOpenThreads (the get_open_threads MCP tool). src/actions.rs approve/deny/destroy (transient-aware) src/auto_update.rs startup rebuild scan + ensure_manager + meta::lock_update_hyperhive bump diff --git a/TODO.md b/TODO.md index c4c2b864..86a825f0 100644 --- a/TODO.md +++ b/TODO.md @@ -40,7 +40,13 @@ how often the friction bites in normal use. into the prompt builder in `hive-ag3nt::turn.rs`. Even better: add a one-shot `recv_batch(max: u32)` MCP tool that returns up to `max` pending messages in a single round-trip. -- ~~**Self-management of own asks + reminders**~~ ✓ landed — unified with `get_loose_ends` (renamed from `get_open_threads` per the naming pass). `LooseEnd` enum (renamed from `OpenThread`) gained a `Reminder { id, owner, message, due_at, age_seconds }` variant (sub-agent flavour filters by `owner == self`; manager unfiltered). New `mcp__hyperhive__cancel_loose_end(kind, id)` on both surfaces — `kind` is `"question"` (asker gets `[cancelled by ]` answer, unblocks) or `"reminder"` (hard-deleted before fire). Auth: sub-agent must own the row; manager bypasses for hive-wide cleanup. New helpers `OperatorQuestions::cancel` + `Broker::cancel_reminder_as` push the auth check down so both flavours stay aligned. Shared dispatch in `hive-c0re/src/questions.rs::handle_cancel_loose_end`. Per-agent web UI's `/api/open-threads` → `/api/loose-ends` too, with reminder-row rendering added. +- **Self-management of own asks + reminders** — once I fire `ask` or + `remind` I have no way to inspect or cancel them from the agent side. + Operator can cancel asks via dashboard; nothing for reminders at all + (TODO above). Want `list_my_asks() -> [{id, target, question, asked_at}]` + and `cancel_ask(id)` on the agent surface, plus `list_my_reminders()` + / `cancel_reminder(id)`. Bounded by `asker == self` and `reminder.owner + == self` so no cross-agent meddling. - **Optional `in_reply_to: ` on send** — pure wire addition; no behavioural change. The dashboard could render conversation threads (already wants this for the agent-to-agent question UI in the diff --git a/hive-ag3nt/assets/app.js b/hive-ag3nt/assets/app.js index 82b6c80f..709d9bd0 100644 --- a/hive-ag3nt/assets/app.js +++ b/hive-ag3nt/assets/app.js @@ -393,43 +393,43 @@ } renderStateBadge(); } - // Loose-ends section: same data the get_loose_ends MCP tool + // Open-threads section: same data the get_open_threads MCP tool // returns. Best-effort fetch on cold load + after every turn_end // (a turn likely answered or asked something). Silent failure // keeps the section hidden rather than surfacing an empty banner. - let lastLooseEndsCount = 0; - async function refreshLooseEnds() { + let lastOpenThreadsCount = 0; + async function refreshOpenThreads() { try { - const resp = await fetch('/api/loose-ends'); + const resp = await fetch('/api/open-threads'); if (!resp.ok) { - renderLooseEnds([]); + renderOpenThreads([]); return; } const data = await resp.json(); - renderLooseEnds(data.loose_ends || []); + renderOpenThreads(data.threads || []); } catch (err) { - console.warn('loose-ends fetch failed', err); - renderLooseEnds([]); + console.warn('open-threads fetch failed', err); + renderOpenThreads([]); } } - function renderLooseEnds(threads) { - const root = $('loose-ends-section'); - const list = $('loose-ends-list'); - const summary = $('loose-ends-summary'); + function renderOpenThreads(threads) { + const root = $('open-threads-section'); + const list = $('open-threads-list'); + const summary = $('open-threads-summary'); if (!root || !list || !summary) return; if (!threads.length) { root.hidden = true; - lastLooseEndsCount = 0; + lastOpenThreadsCount = 0; return; } root.hidden = false; - summary.textContent = 'loose ends · ' + threads.length; + summary.textContent = 'open threads · ' + threads.length; list.innerHTML = ''; // Auto-expand on first appearance of any open thread so the // operator notices new loose ends; collapse only on operator // click (sticky after that). - if (lastLooseEndsCount === 0) root.open = true; - lastLooseEndsCount = threads.length; + if (lastOpenThreadsCount === 0) root.open = true; + lastOpenThreadsCount = threads.length; const fmtAge = (s) => { if (s < 60) return s + 's'; if (s < 3600) return Math.floor(s / 60) + 'm'; @@ -455,18 +455,6 @@ el('span', { class: 'inbox-ts' }, fmtAge(t.age_seconds || 0) + ' ago'), el('div', { class: 'inbox-body' }, t.question || ''), ); - } else if (t.kind === 'reminder') { - // due_at is an absolute unix-seconds value; show time-until-fire - // (negative when overdue, fmtAge handles 0/positive case here). - const now = Math.floor(Date.now() / 1000); - const dueIn = (t.due_at || 0) - now; - const dueLabel = dueIn >= 0 ? 'in ' + fmtAge(dueIn) : fmtAge(-dueIn) + ' overdue'; - li.append( - el('span', { class: 'inbox-from' }, '⏰ reminder #' + t.id), ' ', - el('span', { class: 'inbox-sep' }, t.owner + ' · due ' + dueLabel), ' ', - el('span', { class: 'inbox-ts' }, 'scheduled ' + fmtAge(t.age_seconds || 0) + ' ago'), - el('div', { class: 'inbox-body' }, t.message || ''), - ); } else { li.append(el('span', { class: 'inbox-body' }, JSON.stringify(t))); } @@ -626,7 +614,7 @@ // Open-threads aren't part of /api/state (kept on the broker // db, fetched via the per-agent socket). Cold-load fetches // it here; turn_end refreshes it via the renderer below. - refreshLooseEnds(); + refreshOpenThreads(); // Skip the re-render if nothing structurally changed. The most // common case is `online` polling itself — without this guard, the // operator's gets clobbered every cycle. @@ -968,7 +956,7 @@ } else { setBannerActive(false); setState('idle'); // Likely answered/asked/scheduled something — refresh. - refreshLooseEnds(); + refreshOpenThreads(); } const cls = ev.ok ? 'turn-end-ok' : 'turn-end-fail'; api.row(cls, diff --git a/hive-ag3nt/assets/index.html b/hive-ag3nt/assets/index.html index 7e8dac86..8635a329 100644 --- a/hive-ag3nt/assets/index.html +++ b/hive-ag3nt/assets/index.html @@ -29,9 +29,9 @@ -