Files
Skald-Circle/web/app.js
T
dguiducci 165af19774
Nightly Build / build (push) Successful in 6m58s
tic: run per-user under a system-agent scheduler, with a run log
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.
2026-07-27 11:39:13 +01:00

119 lines
6.3 KiB
JavaScript

import { AppTopbar } from './components/topbar.js';
import { AppSidebar } from './components/sidebar.js';
import { AppCopilot } from './components/copilot.js';
import { LlmProvidersPage } from './components/llm-providers.js';
import { ModelsHubPage } from './components/models-hub.js';
import { ModelsLlmSection } from './components/models-llm.js';
import { ModelsTranscribeSection } from './components/models-transcribe.js';
import { ModelsImageSection } from './components/models-image.js';
import { ModelsTtsSection } from './components/models-tts.js';
import { TasksPage } from './components/tasks/index.js';
import { AgentsPage } from './components/agents.js';
import { UsersPage } from './components/users-page.js';
import { RolesPage } from './components/roles-page.js';
import { SharedFoldersPage } from './components/shared-folders.js';
import { ConnectorsPage } from './components/connectors.js';
import { ConnectorDetailPage } from './components/connector-detail.js';
import { PluginsPage } from './components/plugins-page.js';
import { PluginPageHost } from './components/plugin-page-host.js';
import { PluginCatalogPage } from './components/plugin-catalog.js';
import { PluginDetailPage } from './components/plugin-detail.js';
import { MarketplacePage } from './components/marketplace.js';
import { CatalogPage } from './components/catalog.js';
import { ProfilePage } from './components/profile-page.js';
import { ApprovalGroupsPage } from './components/approval-groups.js';
import { ApprovalRulesPage } from './components/approval-rules.js';
import { ConfigPage } from './components/config-page.js';
import { DashboardPage } from './components/dashboard-page.js';
import { AgentInboxPage } from './components/agent-inbox.js';
import { LlmRequestsPage } from './components/llm-requests.js';
import { LlmRequestDetail } from './components/llm-request-detail.js';
import { SessionDetailPage } from './components/session-detail.js';
import { SystemAgentsPage } from './components/system-agents.js';
import { ProjectsPage } from './components/projects/index.js';
import { FileViewerPage } from './components/file-viewer-page.js';
import { ToolDetailPage } from './components/tool-detail-page.js';
import { SetupPage } from './components/setup-page.js';
import { LoginPage } from './components/login-page.js';
// Register the global `openFile(path)` / `openToolDetail(id)` helpers.
import './lib/open-file.js';
import './lib/open-tool.js';
import { initI18n } from './lib/i18n.js';
customElements.define('app-topbar', AppTopbar);
customElements.define('app-sidebar', AppSidebar);
customElements.define('app-copilot', AppCopilot);
customElements.define('llm-providers-page', LlmProvidersPage);
customElements.define('models-hub-page', ModelsHubPage);
customElements.define('models-llm-section', ModelsLlmSection);
customElements.define('models-transcribe-section', ModelsTranscribeSection);
customElements.define('models-image-section', ModelsImageSection);
customElements.define('models-tts-section', ModelsTtsSection);
customElements.define('tasks-page', TasksPage);
customElements.define('agents-page', AgentsPage);
customElements.define('users-page', UsersPage);
customElements.define('roles-page', RolesPage);
customElements.define('shared-folders-page', SharedFoldersPage);
customElements.define('connectors-page', ConnectorsPage);
customElements.define('connector-detail-page', ConnectorDetailPage);
customElements.define('plugins-page', PluginsPage);
customElements.define('plugin-page-host', PluginPageHost);
customElements.define('plugin-catalog-page', PluginCatalogPage);
customElements.define('plugin-detail-page', PluginDetailPage);
customElements.define('marketplace-page', MarketplacePage);
customElements.define('catalog-page', CatalogPage);
customElements.define('profile-page', ProfilePage);
customElements.define('approval-groups-page', ApprovalGroupsPage);
customElements.define('approval-rules-page', ApprovalRulesPage);
customElements.define('config-page', ConfigPage);
customElements.define('dashboard-page', DashboardPage);
customElements.define('agent-inbox-page', AgentInboxPage);
customElements.define('llm-requests-page', LlmRequestsPage);
customElements.define('llm-request-detail', LlmRequestDetail);
customElements.define('session-detail-page', SessionDetailPage);
customElements.define('system-agents-page', SystemAgentsPage);
customElements.define('projects-page', ProjectsPage);
customElements.define('file-viewer-page', FileViewerPage);
customElements.define('tool-detail-page', ToolDetailPage);
customElements.define('setup-page', SetupPage);
customElements.define('login-page', LoginPage);
// Toggle the workspace placeholder when an LLM page opens/closes.
const workspace = document.getElementById('app-workspace');
window.addEventListener('llm-page-change', (e) => {
workspace.style.display = e.detail.page ? 'none' : 'flex';
});
// ── First-run check ─────────────────────────────────────────────────────────
// If no user exists yet, show the setup screen. Otherwise, check if we have
// a valid session: if not, show the login screen. If logged in, show the app.
(async () => {
const app = document.getElementById('app');
const setup = document.querySelector('setup-page');
const login = document.querySelector('login-page');
try {
const setupRes = await fetch('/api/setup/status');
if (setupRes.ok) {
const { needs_setup } = await setupRes.json();
if (needs_setup) {
if (app) app.style.display = 'none';
if (setup) setup.style.display = '';
return;
}
}
} catch { /* fall through to auth check */ }
try {
const meRes = await fetch('/api/auth/me');
if (meRes.status === 401) {
if (app) app.style.display = 'none';
if (login) login.style.display = '';
return;
}
// Logged in: resolve the effective locale (user pref → instance default).
initI18n();
} catch { /* show app by default */ }
})();