feat: a "Run now" button for the memory lints — one pass, for whoever asked
Nightly Build / build (push) Successful in 7m33s
Nightly Build / build (push) Successful in 7m33s
The two memory lints run weekly, which is right for maintenance and wrong for the moment somebody has just reorganised their notes and wants to know what the lint makes of them. Each agent's tab now carries a button that starts one pass immediately, for the caller. It runs as the caller — their pool, their sessions, their hub — so the report lands with the person who asked. The shared lint is the interesting case: its scheduled pass runs as the admin because the shared store belongs to nobody, but a member pressing the button reads the same store and gets the report themselves, which is coherent with shared memory being readable by every member anyway. Two settings are treated differently on purpose. Due-ness is skipped, exactly as manual /compact skips the compactor's token threshold: the interval answers *when*, and a human asking is a good enough answer to that. The Enabled switch is honoured: it answers *whether*, and that one is the admin's. The conversation review gets no button (AgentScope::PerSubject): it is about somebody else and picks its own subjects, so "run it for me" has no meaning. The frontend reads that from the agent's scope, not from a list of ids. A second starter breaks an invariant the scheduler used to hold for free. system_agent_runs::start sweeps any leftover `running` row of the same agent to `failed` before inserting, which was safe only because one sequential loop was the only thing that ever started a pass; a manual run overlapping a scheduled one would have marked a healthy run as interrupted and duplicated its work. So the agent list moves out of the scheduler and onto Skald as SystemAgents, which holds the registry plus an in-flight guard both paths claim through — keyed on what the pass is *about*, so an instance-wide agent is one slot no matter who runs it, and a per-subject review is keyed on the subject rather than on the supervisor lending the runtime. has_work is answered synchronously, before anything is spawned: it leaves no run row, so without that the button would say "started" over a log that never gains a row. Everything after it is spawned — a pass is an LLM turn, and no HTTP request should be held open for one. The run row exists before the browser is answered, so the log itself is the progress surface; the page polls it quietly until the pass leaves `running`.
This commit is contained in:
@@ -15,7 +15,7 @@ use std::time::Duration;
|
||||
use core_api::system_bus::{RecvError, SystemEvent};
|
||||
use tracing::{info, warn};
|
||||
|
||||
use crate::config::{CoreConfig, EventTriageConfig};
|
||||
use crate::config::CoreConfig;
|
||||
use crate::elicitation::ElicitationBridge;
|
||||
use crate::system_agents::{self, AgentRunCtx, AgentScope, SystemAgent};
|
||||
|
||||
@@ -202,20 +202,17 @@ pub(super) fn spawn_user_lifecycle(skald: &Arc<super::Skald>) {
|
||||
/// Spawned after `Skald` is fully built, like [`spawn_user_lifecycle`] and for
|
||||
/// the same reason: it resolves each user's runtime through `Skald::user_context`.
|
||||
/// The back-reference is [`std::sync::Weak`].
|
||||
pub(super) fn spawn_system_agents(skald: &Arc<super::Skald>, event_triage_config: EventTriageConfig) {
|
||||
pub(super) fn spawn_system_agents(skald: &Arc<super::Skald>) {
|
||||
let weak = Arc::downgrade(skald);
|
||||
let shutdown = skald.rt.shutdown_token.clone();
|
||||
let mut sys_rx = skald.rt.system_bus.subscribe();
|
||||
|
||||
// Adding an agent is one line in `system_agents::registry` plus a
|
||||
// `SystemAgent` impl — no loop of its own, which is the whole point: a second
|
||||
// scheduler would be a fourth global bus in disguise.
|
||||
let agents = system_agents::registry(
|
||||
event_triage_config,
|
||||
Arc::clone(&skald.rt.config),
|
||||
Arc::clone(&skald.rt.db),
|
||||
Arc::clone(&skald.rt.system_bus),
|
||||
);
|
||||
// scheduler would be a fourth global bus in disguise. The list is the
|
||||
// instance's (`Skald::system_agents`), not this loop's: the "Run now" button
|
||||
// starts the very same agents, and both go through one in-flight guard.
|
||||
let agents: Vec<Arc<dyn SystemAgent>> = skald.system_agents.all().to_vec();
|
||||
|
||||
// Interval keys, so a change in the UI cuts the current wait short for
|
||||
// whichever agent it belongs to.
|
||||
@@ -422,6 +419,14 @@ async fn subject_pass(skald: &Arc<super::Skald>, agent: &dyn SystemAgent) {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Keyed on the **subject**, not the supervisor who lends the runtime: the
|
||||
// pass is about them, and two supervisors must not review one person twice.
|
||||
let Some(_claim) = skald.system_agents.claim(agent.id(), &subject_id) else {
|
||||
info!(agent = agent.id(), user = %subject_id,
|
||||
"system-agents: skipped — a review of this person is already in progress");
|
||||
continue;
|
||||
};
|
||||
|
||||
let run_ctx = AgentRunCtx {
|
||||
user_id: &host,
|
||||
pool: &ctx.pool,
|
||||
@@ -485,6 +490,18 @@ async fn run_one(
|
||||
return;
|
||||
}
|
||||
|
||||
// Held for the whole pass. The scheduler alone never needed it — it is one
|
||||
// sequential loop — but the "Run now" button starts the same agents, and two
|
||||
// live passes would have the second one's `start` mark the first's row as
|
||||
// interrupted. Losing the race here simply means the work is already being
|
||||
// done.
|
||||
let target = system_agents::SystemAgents::target_of(agent, user_id);
|
||||
let Some(_claim) = skald.system_agents.claim(agent.id(), &target) else {
|
||||
info!(agent = agent.id(), user = %user_id,
|
||||
"system-agents: skipped — a run of this agent is already in progress");
|
||||
return;
|
||||
};
|
||||
|
||||
let run_ctx = AgentRunCtx {
|
||||
user_id,
|
||||
pool: &ctx.pool,
|
||||
|
||||
Reference in New Issue
Block a user