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.
3.6 KiB
Documentation index
This folder is written for you, the assistant, not for the human directly. It is mounted read-only at ~/docs/ in your workspace. Read it when a user asks how the software itself works, wants help configuring something, or asks what's possible — then explain it in your own words, adapted to that person (their technical level, their language, their actual goal). Don't just paste these files back at them.
Keep answers grounded in what's actually enabled and configured for this instance — check with the relevant tool (e.g. list installed/enabled plugins) rather than assuming everything described here is turned on. A feature documented here may not be enabled on this particular instance.
This index will grow over time. Right now it covers memory, projects, system agents and plugins; more sections (agents, connectors, security groups, shared folders…) will be added later.
Features
| Document | What it covers |
|---|---|
| memory.md | Private and shared memory: what goes where, the indexes and history log, why some shared facts can't be changed on request |
| projects.md | Projects: shared folders with their own assistant chat, a live file explorer, and member sharing |
| system-agents.md | Background agents that run on a schedule (TIC): what they watch, why they run per person, why a run can be skipped |
| settings.md | The admin's Config page: interface language, TIC agent, the compaction model picker, debug mode |
Plugins
Plugins are optional add-ons an admin can enable and configure — extra voices, extra ways to reach the assistant (Telegram, a phone app), image generation, long-term memory, remote access, and so on. Each has its own document in plugins/:
| Document | What it adds |
|---|---|
| plugins/comfyui.md | Local image generation via a self-hosted ComfyUI server |
| plugins/elevenlabs.md | Cloud text-to-speech and transcription (ElevenLabs) |
| plugins/whisper_local.md | Local, private speech-to-text (no cloud, no API key) |
| plugins/kokoro_tts.md | Local, lightweight text-to-speech (CPU-only, no API key) |
| plugins/orpheus_tts_3b.md | Local, expressive text-to-speech with emotion tags (needs a GPU) |
| plugins/honcho.md | Long-term cross-session memory via an external Honcho server (opt-in per user) |
| plugins/telegram.md | Chat with the assistant from Telegram |
| plugins/mobile-connector.md | Companion mobile app: Inbox notifications + remote access, end-to-end encrypted |
| plugins/remote_connectivity.md | Reach the web app remotely over a Tailscale mesh network |
General plugin mechanics that apply to all of them:
- An admin enables/disables and configures each plugin from the Plugin catalog (sidebar → Plugins, admin view): one card per plugin, an enable toggle, and a Configure button opening its settings form.
- A plugin only becomes visible to a given user once the admin grants them access — being enabled instance-wide isn't enough by itself (Mobile Connector is the one exception: access there is the device-pairing itself, not a grant list).
- Some plugins add a per-user settings form of their own (e.g. Telegram's pairing code, Honcho's memory opt-in) on that user's own Plugins page — separate from the admin's instance-wide config.
- A plugin can add tools the assistant calls directly (e.g.
set_secret,telegram_pairing), a dedicated sidebar page, or both.