tic: run per-user under a system-agent scheduler, with a run log
Nightly Build / build (push) Successful in 6m58s

Reframe TIC from an ownerless global loop into a per-user system agent.
The events it reads live in each user's own encrypted mcp_events, the
connectors that produced them run in that user's container, and the
notifications go to that user's hub — so the previous design (built
against the ownerless Conversation bundle, writing into system.db and
notifying a hub with no subscribers) was inert by construction.

Core changes
- TicManager owns no timer and no user list. It now exposes
  run_for(user_id, pool, sessions, hub): one tick for one user, over
  deps unpacked from that user's UserContext. Removed from the
  Conversation bundle; Skald::tic_manager() is gone.
- New spawn_system_agents in wiring.rs: one instance-wide loop, spawned
  post-construction with a Weak<Skald> (like spawn_user_lifecycle).
  Each pass walks the directory and runs TIC for one user at a time —
  sequential, because a pass is N container round-trips and N LLM calls
  nobody is waiting on. A ConfigKeyUpdated on the interval key cuts the
  current wait short; enabled is re-read per pass.
- A user whose database is still locked is skipped (normal, not an
  error): the pool is the unlock token, so a user who hasn't logged in
  since restart has no readable events and nowhere to record a skip.
- The configured tic.security_group is re-checked per user through
  run_context::reconcile_group_for_user — a restricted member never
  gets a tool set their role wouldn't grant; unconfigured starts from
  role_default_run_context, never None (None = catch-all = wider).
- New system_agent_runs owner table (no user_id column — the file is
  the owner): start/finish split so a crash leaves a visible 'running'
  row, swept to 'failed' by the next start; safe because the scheduler
  is sequential and single-instance. An idle tick writes nothing.
- counting_notify wraps the notify tool so the run log can report
  notifications emitted without the tool knowing it's counted.
- The session's event channel is drained by a spawned task instead of
  a dropped receiver — the translator awaits its sends and would wedge
  at capacity.

EventLog::{Persist,Discard} on McpManager::new
- mcp_events is an owner table and its only reader (TIC) is per-user,
  so an event is something that happened to someone. The per-user
  runtime gets Persist; the ownerless global runtime gets Discard (its
  pool is system.db, rows would be unattributable and unread).

API + UI
- GET /api/system-agents/runs: the caller's own run history, scoped
  through require_context with no admin override (same promise as the
  rest of the private pool).
- web/components/system-agents.js replaces tic-sessions.js. The old
  #tic debug page inferred runs from leftover ephemeral sessions; the
  new #system-agents page (sidebar group 'extensions', visible to
  everyone — the data is the caller's own) reads the real run log.
- i18n: tic.* keys replaced with system_agents.* in en/it/fr.

Docs
- New docs/system-agents.md (user-facing: what TIC does, why it runs
  per person, why a run can be missing). Updated docs/settings.md and
  docs/index.md.
- agents/tic/AGENT.md reframed per-user: events are that person's,
  memory is user-memory/ (private) — never shared-memory/.
- CLAUDE.md records the system-agents design and the EventLog seam.
This commit is contained in:
2026-07-27 11:39:13 +01:00
parent 305bdbdd2b
commit 165af19774
26 changed files with 1151 additions and 499 deletions
+27 -16
View File
@@ -25,7 +25,7 @@ export default {
'nav.catalog': 'Connectors Catalog',
'nav.config': 'Settings',
'nav.llm_requests': 'LLM Requests',
'nav.tic': 'TIC Sessions',
'nav.system_agents': 'System agents',
// ── Top bar ────────────────────────────────────────────────────────────────
'topbar.profile': 'Profile',
@@ -182,18 +182,18 @@ export default {
'config.set.interface.name': 'Interface',
'config.set.interface.desc': 'Look and feel of the web interface.',
'config.set.tic_agent.name': 'TIC Agent',
'config.set.tic_agent.desc': 'TIC is a background agent that monitors all async events generated by connected MCP servers (new emails, calendar updates, WhatsApp messages, etc.). It reads your notification rules from data/notifications.md and your memory to decide — via an LLM call — which events are worth surfacing. Relevant notifications are forwarded to the home agent set via /sethome.',
'config.set.tic_agent.desc': 'TIC is a background agent that runs for every user, one at a time. For each user it reads the events their own connectors have pushed since the last run (new mail, calendar changes, incoming messages), decides — via an LLM call — which of them are worth surfacing, and sends those to that user as notifications. It reads only that user\'s events and writes only to their own conversation; a user who has not logged in since the last restart is skipped, because their database is still encrypted. Each run is recorded on the System agents page, visible to the user it ran for.',
'config.set.compaction.name': 'Compaction',
'config.set.compaction.desc': 'When a conversation grows too large, older messages are summarised by an LLM to keep the context within limits.',
'config.prop.ui_locale.name': 'Language',
'config.prop.ui_locale.desc': 'Default interface language for the whole instance. Each user can override it on their profile.',
'config.prop.tic__enabled.name': 'Enabled',
'config.prop.tic__enabled.desc': 'Enable or disable the TIC agent. When disabled, no MCP events are processed.',
'config.prop.tic__enabled.desc': 'Enable or disable the TIC agent for the whole instance. When disabled, no events are processed for anyone.',
'config.prop.tic__security_group.name': 'Security Group',
'config.prop.tic__security_group.desc': 'Tool permission group applied to each TIC agent session. Leave empty to use the default group.',
'config.prop.tic__security_group.desc': 'Tool permission group applied to each TIC run. It is re-checked against each user\'s own role: a user whose role does not allow this group runs under their role\'s default group instead. Leave empty to always use the role default.',
'config.prop.tic__interval_minutes.name': 'Check Interval (minutes)',
'config.prop.tic__interval_minutes.desc': 'How often TIC runs, in minutes. Leave empty to use the value from config.yml (tic.interval_secs).',
'config.prop.tic__interval_minutes.desc': 'How often TIC starts a pass over all users, in minutes. Leave empty to use the value from config.yml (tic.interval_secs).',
'config.prop.compaction_model.name': 'Compaction model',
'config.prop.compaction_model.desc': 'Model used to summarise compacted conversations, for the whole instance. A cheap model is usually enough. Leave empty for automatic selection.',
@@ -950,19 +950,30 @@ export default {
'providers.confirm.delete': 'Delete provider "{name}"? All associated models will be deleted too.',
// ── TIC Sessions ───────────────────────────────────────────────────────────
'tic.title': 'TIC Sessions',
'tic.loading': 'Loading…',
'tic.empty': 'No TIC sessions found.',
'tic.total': '{n} total',
'tic.refresh': 'Refresh',
// ── System agents ───────────────────────────────────────────────────────────
'system_agents.title': 'System agents',
'system_agents.subtitle': 'Background agents the assistant runs for you on a schedule. They read the events your connectors receive and notify you when something looks worth your attention.',
'system_agents.loading': 'Loading…',
'system_agents.empty': 'No runs yet.',
'system_agents.empty_hint': 'A run is recorded only when there are new events to look at.',
'system_agents.total': '{n} total',
'system_agents.refresh': 'Refresh',
'tic.table.agent': 'Agent',
'tic.table.started': 'Started',
'tic.table.messages': 'Messages',
'tic.table.last_activity':'Last activity',
'system_agents.table.agent': 'Agent',
'system_agents.table.started': 'Started',
'system_agents.table.status': 'Status',
'system_agents.table.duration': 'Duration',
'system_agents.table.result': 'Result',
'tic.pagination': 'Page {cur} of {pages} — {total} sessions',
'system_agents.status.running': 'Running',
'system_agents.status.completed': 'Completed',
'system_agents.status.failed': 'Failed',
'system_agents.status.cancelled': 'Cancelled',
'system_agents.stat.events_processed': 'events',
'system_agents.stat.notifications_emitted': 'notifications',
'system_agents.pagination': 'Page {cur} of {pages} — {total} runs',
// ── File viewer ─────────────────────────────────────────────────────────────
'fv.back': 'Back',
+27 -16
View File
@@ -25,7 +25,7 @@ export default {
'nav.catalog': 'Catalogue des connecteurs',
'nav.config': 'Paramètres',
'nav.llm_requests': 'Requêtes LLM',
'nav.tic': 'Sessions TIC',
'nav.system_agents': 'Agents système',
// ── Top bar ────────────────────────────────────────────────────────────────
'topbar.profile': 'Profil',
@@ -182,18 +182,18 @@ export default {
'config.set.interface.name': 'Interface',
'config.set.interface.desc': 'Aspect et style de l\'interface web.',
'config.set.tic_agent.name': 'Agent TIC',
'config.set.tic_agent.desc': 'TIC est un agent d\'arrière-plan qui surveille tous les événements asynchrones générés par les serveurs MCP connectés (nouveaux e-mails, mises à jour du calendrier, messages WhatsApp, etc.). Il lit vos règles de notification dans data/notifications.md et votre mémoire pour décider — via un appel LLM — quels événements méritent d\'être signalés. Les notifications pertinentes sont transmises à l\'agent d\'accueil défini via /sethome.',
'config.set.tic_agent.desc': 'TIC est un agent d\'arrière-plan exécuté pour chaque utilisateur, un à la fois. Pour chacun, il lit les événements reçus par ses propres connecteurs depuis la dernière exécution (nouveaux e-mails, changements d\'agenda, messages entrants), décide — via un appel LLM — lesquels méritent d\'être signalés, et les lui envoie sous forme de notifications. Il ne lit que les événements de cet utilisateur et n\'écrit que dans sa propre conversation ; un utilisateur qui ne s\'est pas connecté depuis le dernier redémarrage est ignoré, car sa base de données est encore chiffrée. Chaque exécution est enregistrée sur la page Agents système, visible par l\'utilisateur concerné.',
'config.set.compaction.name': 'Compaction',
'config.set.compaction.desc': 'Lorsqu\'une conversation devient trop longue, les messages les plus anciens sont résumés par un LLM pour garder le contexte dans les limites.',
'config.prop.ui_locale.name': 'Langue',
'config.prop.ui_locale.desc': 'Langue d\'interface par défaut pour l\'ensemble de l\'instance. Chaque utilisateur peut la modifier dans son profil.',
'config.prop.tic__enabled.name': 'Activé',
'config.prop.tic__enabled.desc': 'Activer ou désactiver l\'agent TIC. Lorsqu\'il est désactivé, aucun événement MCP n\'est traité.',
'config.prop.tic__enabled.desc': 'Activer ou désactiver l\'agent TIC pour toute l\'instance. Lorsqu\'il est désactivé, aucun événement n\'est traité pour personne.',
'config.prop.tic__security_group.name': 'Groupe de sécurité',
'config.prop.tic__security_group.desc': 'Groupe de permissions d\'outils appliqué à chaque session de l\'agent TIC. Laissez vide pour utiliser le groupe par défaut.',
'config.prop.tic__security_group.desc': 'Groupe de permissions d\'outils appliqué à chaque exécution de TIC. Il est revérifié selon le rôle de chaque utilisateur : si son rôle n\'autorise pas ce groupe, l\'exécution utilise le groupe par défaut de son rôle. Laissez vide pour toujours utiliser celui du rôle.',
'config.prop.tic__interval_minutes.name': 'Intervalle de vérification (minutes)',
'config.prop.tic__interval_minutes.desc': 'Fréquence d\'exécution de TIC, en minutes. Laissez vide pour utiliser la valeur de config.yml (tic.interval_secs).',
'config.prop.tic__interval_minutes.desc': 'Fréquence à laquelle TIC lance un passage sur tous les utilisateurs, en minutes. Laissez vide pour utiliser la valeur de config.yml (tic.interval_secs).',
'config.prop.compaction_model.name': 'Modèle de compaction',
'config.prop.compaction_model.desc': 'Modèle utilisé pour résumer les conversations compactées, pour toute l\'instance. Un modèle économique suffit généralement. Laissez vide pour une sélection automatique.',
@@ -940,19 +940,30 @@ export default {
'providers.confirm.delete': 'Supprimer le fournisseur "{name}" ? Tous les modèles associés seront également supprimés.',
// ── TIC Sessions ────────────────────────────────────────────────────────────
'tic.title': 'Sessions TIC',
'tic.loading': 'Chargement…',
'tic.empty': 'Aucune session TIC trouvée.',
'tic.total': '{n} total',
'tic.refresh': 'Actualiser',
// ── Agents système ──────────────────────────────────────────────────────────
'system_agents.title': 'Agents système',
'system_agents.subtitle': 'Agents en arrière-plan que l\'assistant exécute pour vous à intervalles réguliers. Ils lisent les événements reçus par vos connecteurs et vous préviennent lorsque quelque chose mérite votre attention.',
'system_agents.loading': 'Chargement…',
'system_agents.empty': 'Aucune exécution.',
'system_agents.empty_hint': 'Une exécution n\'est enregistrée que lorsqu\'il y a de nouveaux événements à examiner.',
'system_agents.total': '{n} total',
'system_agents.refresh': 'Actualiser',
'tic.table.agent': 'Agent',
'tic.table.started': 'Démarrée',
'tic.table.messages': 'Messages',
'tic.table.last_activity':'Dernière activité',
'system_agents.table.agent': 'Agent',
'system_agents.table.started': 'Démarrée',
'system_agents.table.status': 'Statut',
'system_agents.table.duration': 'Durée',
'system_agents.table.result': 'Résultat',
'tic.pagination': 'Page {cur} sur {pages} — {total} sessions',
'system_agents.status.running': 'En cours',
'system_agents.status.completed': 'Terminée',
'system_agents.status.failed': 'Échouée',
'system_agents.status.cancelled': 'Annulée',
'system_agents.stat.events_processed': 'événements',
'system_agents.stat.notifications_emitted': 'notifications',
'system_agents.pagination': 'Page {cur} sur {pages} — {total} exécutions',
// ── File viewer ─────────────────────────────────────────────────────────────
'fv.back': 'Retour',
+27 -16
View File
@@ -25,7 +25,7 @@ export default {
'nav.catalog': 'Catalogo connettori',
'nav.config': 'Impostazioni',
'nav.llm_requests': 'Richieste LLM',
'nav.tic': 'Sessioni TIC',
'nav.system_agents': 'Agenti di sistema',
// ── Barra superiore ────────────────────────────────────────────────────────
'topbar.profile': 'Profilo',
@@ -206,18 +206,18 @@ export default {
'config.set.interface.name': 'Interfaccia',
'config.set.interface.desc': 'Aspetto e stile dell\'interfaccia web.',
'config.set.tic_agent.name': 'Agente TIC',
'config.set.tic_agent.desc': 'TIC è un agente in background che monitora tutti gli eventi asincroni generati dai server MCP connessi (nuove email, aggiornamenti del calendario, messaggi WhatsApp, ecc.). Legge le regole di notifica da data/notifications.md e la memoria per decidere — tramite una chiamata LLM — quali eventi vale la pena segnalare. Le notifiche rilevanti vengono inoltrate all\'agente predefinito impostato tramite /sethome.',
'config.set.tic_agent.desc': 'TIC è un agente in background che viene eseguito per ogni utente, uno alla volta. Per ciascun utente legge gli eventi che i suoi connettori hanno ricevuto dall\'ultima esecuzione (nuove email, modifiche al calendario, messaggi in arrivo), decide — tramite una chiamata LLM — quali meritano attenzione e glieli inoltra come notifiche. Legge solo gli eventi di quell\'utente e scrive solo nella sua conversazione; un utente che non ha effettuato l\'accesso dall\'ultimo riavvio viene saltato, perché il suo database è ancora cifrato. Ogni esecuzione viene registrata nella pagina Agenti di sistema, visibile all\'utente per cui è stata eseguita.',
'config.set.compaction.name': 'Compattazione',
'config.set.compaction.desc': 'Quando una conversazione diventa troppo lunga, i messaggi più vecchi vengono riassunti da un LLM per mantenere il contesto entro i limiti.',
'config.prop.ui_locale.name': 'Lingua',
'config.prop.ui_locale.desc': 'Lingua predefinita per l\'intera istanza. Ogni utente può modificarla nel proprio profilo.',
'config.prop.tic__enabled.name': 'Attivo',
'config.prop.tic__enabled.desc': 'Attiva o disattiva l\'agente TIC. Quando disattivato, nessun evento MCP viene elaborato.',
'config.prop.tic__enabled.desc': 'Attiva o disattiva l\'agente TIC per l\'intera istanza. Quando è disattivato, non viene elaborato alcun evento per nessuno.',
'config.prop.tic__security_group.name': 'Gruppo di sicurezza',
'config.prop.tic__security_group.desc': 'Gruppo di permessi strumenti applicato a ogni sessione dell\'agente TIC. Lascia vuoto per usare il gruppo predefinito.',
'config.prop.tic__security_group.desc': 'Gruppo di permessi strumenti applicato a ogni esecuzione di TIC. Viene riverificato sul ruolo di ciascun utente: se il ruolo non consente questo gruppo, l\'esecuzione usa il gruppo predefinito del ruolo. Lascia vuoto per usare sempre il predefinito del ruolo.',
'config.prop.tic__interval_minutes.name': 'Intervallo di controllo (minuti)',
'config.prop.tic__interval_minutes.desc': 'Ogni quanto TIC viene eseguito, in minuti. Lascia vuoto per usare il valore da config.yml (tic.interval_secs).',
'config.prop.tic__interval_minutes.desc': 'Ogni quanto TIC avvia un giro su tutti gli utenti, in minuti. Lascia vuoto per usare il valore da config.yml (tic.interval_secs).',
'config.prop.compaction_model.name': 'Modello per la compattazione',
'config.prop.compaction_model.desc': 'Modello usato per riassumere le conversazioni compattate, per tutta l\'istanza. Un modello economico di solito è sufficiente. Lascia vuoto per la selezione automatica.',
@@ -940,19 +940,30 @@ export default {
'providers.confirm.delete': 'Eliminare il provider "{name}"? Tutti i modelli associati verranno eliminati.',
// ── TIC Sessions ────────────────────────────────────────────────────────────
'tic.title': 'Sessioni TIC',
'tic.loading': 'Caricamento…',
'tic.empty': 'Nessuna sessione TIC trovata.',
'tic.total': '{n} totale',
'tic.refresh': 'Aggiorna',
// ── Agenti di sistema ───────────────────────────────────────────────────────
'system_agents.title': 'Agenti di sistema',
'system_agents.subtitle': 'Agenti in background che l\'assistente esegue per te a intervalli regolari. Leggono gli eventi che arrivano dai tuoi connettori e ti avvisano quando c\'è qualcosa che merita attenzione.',
'system_agents.loading': 'Caricamento…',
'system_agents.empty': 'Nessuna esecuzione.',
'system_agents.empty_hint': 'Un\'esecuzione viene registrata solo quando ci sono nuovi eventi da esaminare.',
'system_agents.total': '{n} totale',
'system_agents.refresh': 'Aggiorna',
'tic.table.agent': 'Agente',
'tic.table.started': 'Iniziata',
'tic.table.messages': 'Messaggi',
'tic.table.last_activity':'Ultima attività',
'system_agents.table.agent': 'Agente',
'system_agents.table.started': 'Iniziata',
'system_agents.table.status': 'Esito',
'system_agents.table.duration': 'Durata',
'system_agents.table.result': 'Risultato',
'tic.pagination': 'Pagina {cur} di {pages} — {total} sessioni',
'system_agents.status.running': 'In corso',
'system_agents.status.completed': 'Completata',
'system_agents.status.failed': 'Fallita',
'system_agents.status.cancelled': 'Annullata',
'system_agents.stat.events_processed': 'eventi',
'system_agents.stat.notifications_emitted': 'notifiche',
'system_agents.pagination': 'Pagina {cur} di {pages} — {total} esecuzioni',
// ── File viewer ──────────────────────────────────────────────────────────────
'fv.back': 'Indietro',