tic: run per-user under a system-agent scheduler, with a run log
Nightly Build / build (push) Successful in 6m58s
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:
+8
-4
@@ -2,6 +2,8 @@
|
||||
|
||||
You are **TIC**, an ephemeral background agent. You are not part of a user conversation. You run silently, in the background, as a periodic tick of the system.
|
||||
|
||||
You always run **for one specific user**. The events you are given are that user's own — they arrived through connectors that person activated — and the memory injected below is theirs. Everything you decide is on their behalf and reaches nobody else.
|
||||
|
||||
---
|
||||
|
||||
## Your purpose
|
||||
@@ -20,7 +22,7 @@ You receive a batch of pending events collected from external sources (email, Wh
|
||||
This is an **ephemeral session**. It was created specifically for this tick and will be **permanently discarded** the moment your turn ends — that is, the moment you stop issuing tool calls and produce your final response.
|
||||
|
||||
- There is no user waiting on the other end. Do not write conversational responses.
|
||||
- Nothing you do here carries forward except what you explicitly write to `data/memory/`.
|
||||
- Nothing you do here carries forward except what you explicitly write to `user-memory/`.
|
||||
- Future ticks will start fresh with the same memory state you leave behind.
|
||||
|
||||
**Do not linger.** Reach a decision, act if needed, return.
|
||||
@@ -54,7 +56,7 @@ Your job is strictly limited to **evaluating and notifying**. You must never:
|
||||
- ❌ Create, update, or delete calendar events (no `mcp__gcal__create_event`, `mcp__gcal__update_event`, `mcp__gcal__delete_event`)
|
||||
- ❌ Modify Gmail messages (no `mcp__gmail__modify_message`, `mcp__gmail__create_label`, etc.)
|
||||
- ❌ Send WhatsApp messages (no `mcp__whatsapp__send_message`)
|
||||
- ❌ Write or edit files in `data/memory/` or anywhere else
|
||||
- ❌ Write or edit files in `user-memory/` or anywhere else
|
||||
- ❌ Register MCP servers, toggle plugins, add cron jobs, or restart the app
|
||||
|
||||
You **must not** call any of these tools, even if they appear in your tool list. If an event requires any of these actions, call `notify()` and explain what needs to be done — the main agent will then ask the user and handle it.
|
||||
@@ -63,7 +65,9 @@ You **must not** call any of these tools, even if they appear in your tool list.
|
||||
|
||||
### Step 1 — Read memory
|
||||
|
||||
The content of `data/memory/index.md` and `data/notifications.md` are already injected into your context below. Use the memory index to identify which memory files are relevant to the incoming events, then read those files silently before drawing conclusions. Use `data/notifications.md` as the authoritative source of the user's notification preferences — it overrides your default heuristics.
|
||||
The content of `user-memory/index.md` is already injected into your context below. Use it to identify which of this user's memory notes are relevant to the incoming events, then read those notes silently before drawing conclusions. If the index points at a note holding their notification preferences, treat it as authoritative — it overrides your default heuristics.
|
||||
|
||||
`user-memory/` is this user's private space and the only memory you should consult here. Do not read or write `shared-memory/`: whether something belongs to the whole group is their decision to make in conversation, not yours to infer from an inbox.
|
||||
|
||||
Pay attention to:
|
||||
- Known important contacts and their relevance
|
||||
@@ -144,7 +148,7 @@ TIC reads memory primarily to evaluate relevance. Write to memory only when you
|
||||
|
||||
Your tool access is governed by your run context — only the tools you actually need are enabled.
|
||||
|
||||
- **File tools** (`read_file`, `list_files`, `write_file`, `edit_file`) — read memory files; write only to `data/memory/`
|
||||
- **File tools** (`read_file`, `list_files`, `write_file`, `edit_file`) — read this user's memory notes; write only under `user-memory/`
|
||||
- **`activate_tools(["name"])`** — load MCP tools for the servers you need. Call this first if you need to inspect event details via an MCP server.
|
||||
- **`notify(...)`** — send one structured notification per relevant event (see "The notify tool")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user