feat(event-triage): per-user check interval, overriding the instance one
Nightly Build / build (push) Successful in 5m4s
Nightly Build / build (push) Successful in 5m4s
Event triage is the one system agent whose right cadence depends on who it
runs for: it fires on inbound events, so someone on a dozen mailing lists
has something waiting on nearly every tick while a quiet account has
something waiting almost never. A single instance-wide interval serves one
of them badly, and the observed failure is the first: the agent starts on
practically every pass.
An admin can now set a per-person interval on that user's page (Users ->
the person -> Event triage). Empty means "follow the instance setting",
which stays the state nobody has a row for.
- New registry table `system_agent_user_settings(agent_id, user_id,
interval_secs)`. A row is an override and its absence is inheritance --
no sentinel value, no row seeded at user creation, clearing the field
deletes the row. Registry rather than the user's own file because the
writer is the admin and a member's database is unreadable unless they
happen to be logged in; a setting that could only be changed during its
subject's session would not be a setting. Keyed by agent_id though only
one agent uses it, so a future agent's schedule is not a schema change.
- `SystemAgent` gains `interval_secs_for(user_id)`, which `is_due` now
measures against, and `shortest_interval_secs()`. Both default to the
existing `interval_secs`, so every other agent implements nothing. The
second is the non-obvious half: `base_tick` sleeps for the shortest
interval any enabled agent asks for, so without it an override below the
instance value would be rounded up to it -- an override that works when
it lengthens and silently does nothing when it shortens.
- `GET/PUT /api/users/{id}/event-triage`, admin-gated, minutes on the
wire, null to clear. Nothing rides the bus: the scheduler re-reads the
interval every tick and due-ness is counted from the user's own last
attempt, so a change lands on the next wake-up with no push.
Both helpers fail open onto the instance value -- an unreadable registry
must not turn into an agent that stops running for someone.
Docs: docs/system-agents.md gains the per-person section and no longer
reads as if the interval were one number for everybody.
This commit is contained in:
@@ -35,6 +35,7 @@ pub mod supervision;
|
||||
pub mod system_agent_coverage;
|
||||
pub mod system_agent_runs;
|
||||
pub mod system_agent_state;
|
||||
pub mod system_agent_user_settings;
|
||||
pub mod tool_permission_groups;
|
||||
pub mod user_config;
|
||||
pub mod users;
|
||||
@@ -192,7 +193,7 @@ async fn ensure_column(pool: &SqlitePool, table: &str, column: &str, decl: &str)
|
||||
// Instance-wide, readable without any user key: the directory you must open
|
||||
// before you know who exists. Nothing here is scoped to one user.
|
||||
|
||||
async fn create_registry_tables(pool: &SqlitePool) -> Result<()> {
|
||||
pub(crate) async fn create_registry_tables(pool: &SqlitePool) -> Result<()> {
|
||||
sqlx::query(
|
||||
"CREATE TABLE IF NOT EXISTS llm_providers (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
@@ -760,6 +761,34 @@ async fn create_registry_tables(pool: &SqlitePool) -> Result<()> {
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
// Per-user overrides of a system agent's schedule. **A row is an override and
|
||||
// nothing else** — its absence means "use the instance-wide setting", which is
|
||||
// why there is no `inherit` flag and no row written at user creation.
|
||||
//
|
||||
// Registry rather than owner, and not for the reason `system_agent_coverage`
|
||||
// is: this one is written *by the admin about a member*, on the Users page,
|
||||
// and a member's own file is unreadable unless they happen to be logged in
|
||||
// (§9). A setting an admin can only change while its subject has a live
|
||||
// session would not be a setting. It is admin-readable, like the rest of the
|
||||
// directory metadata next to it, and holds no content — a number of seconds.
|
||||
//
|
||||
// `agent_id` is bare TEXT with no `system_agent_*` table to reference (the
|
||||
// agents are code, not rows), and is kept in the key even though only event
|
||||
// triage uses it today: the alternative is a column per agent on `users`, and
|
||||
// "a fourth agent is a trait impl plus one registry line" would stop being
|
||||
// true the moment its schedule needed a schema change.
|
||||
sqlx::query(
|
||||
"CREATE TABLE IF NOT EXISTS system_agent_user_settings (
|
||||
agent_id TEXT NOT NULL,
|
||||
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
interval_secs INTEGER,
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
PRIMARY KEY (agent_id, user_id)
|
||||
)",
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user