agent-orchestrator/docs/architecture.md
Hibryda a89e2b9f69 docs: restructure docs — eliminate v3- prefix, merge findings, create decisions.md
Merge v3-task_plan.md content into architecture.md (data model, layout system,
keyboard shortcuts) and new decisions.md (22-entry categorized decisions log).
Merge v3-findings.md into unified findings.md (16 sections covering all research).
Move progress logs to progress/ subdirectory (v2.md, v3.md, v2-archive.md).
Rename v3-release-notes.md to release-notes.md. Update all cross-references.
Delete v3-task_plan.md and v3-findings.md (content fully incorporated).
2026-03-14 02:51:13 +01:00

24 KiB

System Architecture

This document describes the end-to-end architecture of Agent Orchestrator — how the Rust backend, Svelte 5 frontend, and Node.js/Deno sidecar processes work together to provide a multi-project AI agent orchestration dashboard.


High-Level Overview

Agent Orchestrator is a Tauri 2.x desktop application. Tauri provides a Rust backend process and a WebKit2GTK-based webview for the frontend. The application manages AI agent sessions by spawning sidecar child processes that communicate with AI provider APIs (Claude, Codex, Ollama).

┌────────────────────────────────────────────────────────────────┐
│  Agent Orchestrator (Tauri 2.x)                                │
│                                                                │
│  ┌─────────────────┐    Tauri IPC     ┌────────────────────┐  │
│  │  WebView         │ ◄─────────────► │  Rust Backend      │  │
│  │  (Svelte 5)      │   invoke/listen │                    │  │
│  │                   │                │  ├── PtyManager     │  │
│  │  ├── ProjectGrid  │                │  ├── SidecarManager │  │
│  │  ├── AgentPane    │                │  ├── SessionDb      │  │
│  │  ├── TerminalPane │                │  ├── BtmsgDb        │  │
│  │  ├── StatusBar    │                │  ├── SearchDb       │  │
│  │  └── Stores       │                │  ├── SecretsManager │  │
│  └─────────────────┘                 │  ├── RemoteManager  │  │
│                                       │  └── FileWatchers   │  │
│                                       └────────────────────┘  │
│                                           │                    │
└───────────────────────────────────────────┼────────────────────┘
                                            │ stdio NDJSON
                                            ▼
                                    ┌───────────────────┐
                                    │ Sidecar Processes  │
                                    │ (Deno or Node.js)  │
                                    │                    │
                                    │ claude-runner.mjs  │
                                    │ codex-runner.mjs   │
                                    │ ollama-runner.mjs  │
                                    └───────────────────┘

Why Three Layers?

  1. Rust backend — Manages OS-level resources (PTY processes, file watchers, SQLite databases) with memory safety and low overhead. Exposes everything to the frontend via Tauri IPC commands and events.

  2. Svelte 5 frontend — Renders the UI with fine-grained reactivity (no VDOM). Svelte 5 runes ($state, $derived, $effect) provide signal-based reactivity comparable to Solid.js but with a larger ecosystem.

  3. Sidecar processes — The Claude Agent SDK, OpenAI Codex SDK, and Ollama API are all JavaScript/TypeScript libraries. They cannot run in Rust or in the WebKit2GTK webview (no Node.js APIs). The sidecar layer bridges this gap: Rust spawns a JS process, communicates via stdio NDJSON, and forwards structured messages to the frontend.


Rust Backend (v2/src-tauri/)

The Rust backend is the central coordinator. It owns all OS resources and database connections.

Cargo Workspace

The Rust code is organized as a Cargo workspace with three members:

v2/
├── Cargo.toml              # Workspace root
├── bterminal-core/          # Shared crate
│   └── src/
│       ├── lib.rs
│       ├── pty.rs           # PtyManager (portable-pty)
│       ├── sidecar.rs       # SidecarManager (multi-provider)
│       ├── supervisor.rs    # SidecarSupervisor (crash recovery)
│       ├── sandbox.rs       # Landlock sandbox
│       └── event.rs         # EventSink trait
├── bterminal-relay/         # Remote machine relay
│   └── src/main.rs          # WebSocket server + token auth
└── src-tauri/               # Tauri application
    └── src/
        ├── lib.rs           # AppState + setup + handler registration
        ├── commands/        # 16 domain command modules
        ├── btmsg.rs         # Inter-agent messaging (SQLite)
        ├── bttask.rs        # Task board (SQLite, shared btmsg.db)
        ├── search.rs        # FTS5 full-text search
        ├── secrets.rs       # System keyring (libsecret)
        ├── plugins.rs       # Plugin discovery
        ├── notifications.rs # Desktop notifications
        ├── session/         # SessionDb (sessions, layout, settings, agents, metrics, anchors)
        ├── remote.rs        # RemoteManager (WebSocket client)
        ├── ctx.rs           # Read-only ctx database access
        ├── memora.rs        # Read-only Memora database access
        ├── telemetry.rs     # OpenTelemetry tracing
        ├── groups.rs        # Project groups config
        ├── watcher.rs       # File watcher (notify crate)
        ├── fs_watcher.rs    # Per-project filesystem watcher (inotify)
        ├── event_sink.rs    # TauriEventSink implementation
        ├── pty.rs           # Thin re-export from bterminal-core
        └── sidecar.rs       # Thin re-export from bterminal-core

Why a Workspace?

The bterminal-core crate exists so that both the Tauri application and the standalone bterminal-relay binary can share PtyManager and SidecarManager code. The EventSink trait abstracts event emission — TauriEventSink wraps Tauri's AppHandle, while the relay uses a WebSocket-based EventSink.

AppState

All backend state lives in AppState, initialized during Tauri setup:

pub struct AppState {
    pub pty_manager: Mutex<PtyManager>,
    pub sidecar_manager: Mutex<SidecarManager>,
    pub session_db: Mutex<SessionDb>,
    pub remote_manager: Mutex<RemoteManager>,
    pub telemetry: Option<TelemetryGuard>,
}

SQLite Databases

The backend manages two SQLite databases, both in WAL mode with 5-second busy timeout for concurrent access:

Database Location Purpose
sessions.db ~/.local/share/bterminal/ Sessions, layout, settings, agent state, metrics, anchors
btmsg.db ~/.local/share/bterminal/ Inter-agent messages, tasks, agents registry, audit log

WAL checkpoints run every 5 minutes via a background tokio task to prevent unbounded WAL growth.

All queries use named column access (row.get("column_name")) — never positional indices. Rust structs use #[serde(rename_all = "camelCase")] so TypeScript interfaces receive camelCase field names on the wire.

Command Modules

Tauri commands are organized into 16 domain modules under commands/:

Module Commands Purpose
pty spawn, write, resize, kill Terminal management
agent query, stop, ready, restart Agent session lifecycle
session session CRUD, layout, settings Session persistence
persistence agent state, messages Agent session continuity
knowledge ctx, memora queries External knowledge bases
claude profiles, skills Claude-specific features
groups load, save Project group config
files list_directory, read/write file File browser
watcher start, stop File change monitoring
remote 12 commands Remote machine management
bttask list, create, update, delete, comments Task board
search init, search, rebuild, index FTS5 search
secrets store, get, delete, list, has_keyring Secrets management
plugins discover, read_file Plugin discovery
notifications send_desktop OS notifications
misc test_mode, frontend_log Utilities

Svelte 5 Frontend (v2/src/)

The frontend uses Svelte 5 with runes for reactive state management. The UI follows a VSCode-inspired layout with a left icon rail, expandable drawer, project grid, and status bar.

Component Hierarchy

App.svelte                           [Root — VSCode-style layout]
├── CommandPalette.svelte            [Ctrl+K overlay, 18+ commands]
├── SearchOverlay.svelte             [Ctrl+Shift+F, FTS5 Spotlight-style]
├── NotificationCenter.svelte        [Bell icon + dropdown]
├── GlobalTabBar.svelte              [Left icon rail, 2.75rem wide]
├── [Sidebar Panel]                  [Expandable drawer, max 50%]
│   └── SettingsTab.svelte           [Global settings + group/project CRUD]
├── ProjectGrid.svelte               [Flex + scroll-snap, adaptive count]
│   └── ProjectBox.svelte            [Per-project container, 11 tab types]
│       ├── ProjectHeader.svelte     [Icon + name + status + badges]
│       ├── AgentSession.svelte      [Main Claude session wrapper]
│       │   ├── AgentPane.svelte     [Structured message rendering]
│       │   └── TeamAgentsPanel.svelte [Tier 1 subagent cards]
│       ├── TerminalTabs.svelte      [Shell/SSH/agent-preview tabs]
│       │   ├── TerminalPane.svelte  [xterm.js + Canvas addon]
│       │   └── AgentPreviewPane.svelte [Read-only agent activity]
│       ├── DocsTab.svelte           [Markdown file browser]
│       ├── ContextTab.svelte        [LLM context visualization]
│       ├── FilesTab.svelte          [Directory tree + CodeMirror editor]
│       ├── SshTab.svelte            [SSH connection manager]
│       ├── MemoriesTab.svelte       [Memora database viewer]
│       ├── MetricsPanel.svelte      [Health + history sparklines]
│       ├── TaskBoardTab.svelte      [Kanban board, Manager only]
│       ├── ArchitectureTab.svelte   [PlantUML viewer, Architect only]
│       └── TestingTab.svelte        [Selenium/test files, Tester only]
└── StatusBar.svelte                 [Agent counts, burn rate, attention queue]

Stores (Svelte 5 Runes)

All store files use the .svelte.ts extension — this is required for Svelte 5 runes ($state, $derived, $effect). Files with plain .ts extension will compile but fail at runtime with "rune_outside_svelte".

Store Purpose
workspace.svelte.ts Project groups, active group, tabs, focus
agents.svelte.ts Agent sessions, messages, cost, parent/child hierarchy
health.svelte.ts Per-project health tracking, attention scoring, burn rate
conflicts.svelte.ts File overlap + external write detection
anchors.svelte.ts Session anchor management (auto/pinned/promoted)
notifications.svelte.ts Toast + history (6 types, unread badge)
plugins.svelte.ts Plugin command registry, event bus
theme.svelte.ts 17 themes, font restoration
machines.svelte.ts Remote machine state
wake-scheduler.svelte.ts Manager auto-wake (3 strategies, per-manager timers)

Adapters (IPC Bridge Layer)

Adapters wrap Tauri invoke() calls and listen() event subscriptions. They isolate the frontend from IPC details and provide typed TypeScript interfaces.

Adapter Backend Module Purpose
agent-bridge.ts sidecar + commands/agent Agent query/stop/restart
pty-bridge.ts pty + commands/pty Terminal spawn/write/resize
claude-messages.ts — (frontend-only) Parse Claude SDK NDJSON → AgentMessage
codex-messages.ts — (frontend-only) Parse Codex ThreadEvents → AgentMessage
ollama-messages.ts — (frontend-only) Parse Ollama chunks → AgentMessage
message-adapters.ts — (frontend-only) Provider registry for message parsers
provider-bridge.ts commands/claude Generic provider bridge (profiles, skills)
btmsg-bridge.ts btmsg Inter-agent messaging
bttask-bridge.ts bttask Task board operations
groups-bridge.ts groups Group config load/save
session-bridge.ts session Session/layout persistence
settings-bridge.ts session/settings Key-value settings
files-bridge.ts commands/files File browser operations
search-bridge.ts search FTS5 search
secrets-bridge.ts secrets System keyring
anchors-bridge.ts session/anchors Session anchor CRUD
remote-bridge.ts remote Remote machine management
ssh-bridge.ts session/ssh SSH session CRUD
ctx-bridge.ts ctx Context database queries
memora-bridge.ts memora Memora database queries
fs-watcher-bridge.ts fs_watcher Filesystem change events
audit-bridge.ts btmsg (audit_log) Audit log queries
telemetry-bridge.ts telemetry Frontend → Rust tracing
notifications-bridge.ts notifications Desktop notification trigger
plugins-bridge.ts plugins Plugin discovery

Agent Dispatcher

The agent dispatcher (agent-dispatcher.ts, ~260 lines) is the central router between sidecar events and the agent store. When the Rust backend emits a sidecar-message Tauri event, the dispatcher:

  1. Looks up the provider for the session (via sessionProviderMap)
  2. Routes the raw message through the appropriate adapter (claude-messages.ts, codex-messages.ts, or ollama-messages.ts) via message-adapters.ts
  3. Feeds the resulting AgentMessage[] into the agent store
  4. Handles side effects: subagent pane spawning, session persistence, auto-anchoring, worktree detection, health tracking, conflict recording

The dispatcher delegates to four extracted utility modules:

  • utils/session-persistence.ts — session-project maps, persistSessionForProject
  • utils/subagent-router.ts — spawn + route subagent panes
  • utils/auto-anchoring.ts — triggerAutoAnchor on first compaction event
  • utils/worktree-detection.ts — detectWorktreeFromCwd pure function

Sidecar Layer (v2/sidecar/)

See sidecar.md for the full sidecar architecture. In brief:

  • Each AI provider has its own runner file (e.g., claude-runner.ts) compiled to an ESM bundle (claude-runner.mjs) by esbuild
  • Rust's SidecarManager spawns the appropriate runner based on the provider field in AgentQueryOptions
  • Communication uses stdio NDJSON — one JSON object per line, newline-delimited
  • Deno is preferred (faster startup), Node.js is the fallback
  • The Claude runner uses @anthropic-ai/claude-agent-sdk query() internally

Data Flow: Agent Query Lifecycle

Here is the complete path of a user prompt through the system:

1. User types prompt in AgentPane
2. AgentPane calls agentBridge.queryAgent(options)
3. agent-bridge.ts invokes Tauri command 'agent_query'
4. Rust agent_query handler calls SidecarManager.query()
5. SidecarManager resolves provider runner (e.g., claude-runner.mjs)
6. SidecarManager writes QueryMessage as NDJSON to sidecar stdin
7. Sidecar runner calls provider SDK (e.g., Claude Agent SDK query())
8. Provider SDK streams responses
9. Runner forwards each response as NDJSON to stdout
10. SidecarManager reads stdout line-by-line
11. SidecarManager emits Tauri event 'sidecar-message' with sessionId + data
12. Frontend agent-dispatcher.ts receives event
13. Dispatcher routes through message-adapters.ts → provider-specific parser
14. Parser converts to AgentMessage[]
15. Dispatcher feeds messages into agents.svelte.ts store
16. AgentPane reactively re-renders via $derived bindings

Session Stop Flow

1. User clicks Stop button in AgentPane
2. AgentPane calls agentBridge.stopAgent(sessionId)
3. agent-bridge.ts invokes Tauri command 'agent_stop'
4. Rust handler calls SidecarManager.stop(sessionId)
5. SidecarManager writes StopMessage to sidecar stdin
6. Runner calls AbortController.abort() on the SDK query
7. SDK terminates the Claude subprocess
8. Runner emits final status message, then closes

Configuration

Project Groups (~/.config/bterminal/groups.json)

Human-editable JSON file defining project groups and their projects. Loaded at startup by groups.rs. Not hot-reloaded — changes require app restart or group switch.

SQLite Settings (sessions.dbsettings table)

Key-value store for user preferences: theme, fonts, shell, CWD, provider settings. Accessed via settings-bridge.tssettings_get/settings_set Tauri commands.

Environment Variables

Variable Purpose
BTERMINAL_TEST Enables test mode (disables watchers, wake scheduler)
BTERMINAL_TEST_DATA_DIR Redirects SQLite database storage
BTERMINAL_TEST_CONFIG_DIR Redirects groups.json config
BTERMINAL_OTLP_ENDPOINT Enables OpenTelemetry OTLP export

Data Model

Project Group Config (~/.config/bterminal/groups.json)

Human-editable JSON file defining workspaces. Each group contains up to 5 projects. Loaded at startup by groups.rs, not hot-reloaded.

{
  "version": 1,
  "groups": [
    {
      "id": "work-ai",
      "name": "AI Projects",
      "projects": [
        {
          "id": "bterminal",
          "name": "BTerminal",
          "identifier": "bterminal",
          "description": "Terminal emulator with Claude integration",
          "icon": "\uf120",
          "cwd": "/home/user/code/BTerminal",
          "profile": "default",
          "enabled": true
        }
      ]
    }
  ],
  "activeGroupId": "work-ai"
}

TypeScript Types (v2/src/lib/types/groups.ts)

export interface ProjectConfig {
  id: string;
  name: string;
  identifier: string;
  description: string;
  icon: string;
  cwd: string;
  profile: string;
  enabled: boolean;
}

export interface GroupConfig {
  id: string;
  name: string;
  projects: ProjectConfig[];  // max 5
}

export interface GroupsFile {
  version: number;
  groups: GroupConfig[];
  activeGroupId: string;
}

SQLite Schema (v3 Additions)

Beyond the core sessions and settings tables, v3 added project-scoped agent persistence:

ALTER TABLE sessions ADD COLUMN project_id TEXT DEFAULT '';

CREATE TABLE IF NOT EXISTS agent_messages (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    session_id TEXT NOT NULL,
    project_id TEXT NOT NULL,
    sdk_session_id TEXT,
    message_type TEXT NOT NULL,
    content TEXT NOT NULL,
    parent_id TEXT,
    created_at INTEGER NOT NULL,
    FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
);

CREATE TABLE IF NOT EXISTS project_agent_state (
    project_id TEXT PRIMARY KEY,
    last_session_id TEXT NOT NULL,
    sdk_session_id TEXT,
    status TEXT NOT NULL,
    cost_usd REAL DEFAULT 0,
    input_tokens INTEGER DEFAULT 0,
    output_tokens INTEGER DEFAULT 0,
    last_prompt TEXT,
    updated_at INTEGER NOT NULL
);

Layout System

Project Grid (Flexbox + scroll-snap)

Projects are arranged horizontally in a flex container with CSS scroll-snap for clean project-to-project scrolling:

.project-grid {
  display: flex;
  gap: 4px;
  height: 100%;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
}

.project-box {
  flex: 0 0 calc((100% - (N-1) * 4px) / N);
  scroll-snap-align: start;
  min-width: 480px;
}

N is computed from viewport width: Math.min(projects.length, Math.max(1, Math.floor(containerWidth / 520)))

Project Box Internal Layout

Each project box uses a CSS grid with 4 rows:

┌─ ProjectHeader (auto) ─────────────────┐
├─────────────────────┬──────────────────┤
│ AgentSession        │ TeamAgentsPanel  │
│ (flex: 1)           │ (240px/overlay)  │
├─────────────────────┴──────────────────┤
│ [Tab1] [Tab2] [+]          TabBar auto │
├────────────────────────────────────────┤
│ Terminal content (xterm or scrollback) │
└────────────────────────────────────────┘

Team panel: inline at >2560px viewport (240px wide), overlay at <2560px. Collapsed when no subagents running.

Responsive Breakpoints

Viewport Width Visible Projects Team Panel Mode
5120px+ 5 inline 240px
3840px 4 inline 200px
2560px 3 overlay
1920px 3 overlay
<1600px 1 + project tabs overlay

xterm.js Budget: 4 Active Instances

WebKit2GTK OOMs at ~5 simultaneous xterm.js instances. The budget system manages this:

State xterm.js Instance? Memory
Active-Focused Yes ~20MB
Active-Background Yes (if budget allows) ~20MB
Suspended No (HTML pre scrollback) ~200KB
Uninitialized No (placeholder) 0

On focus: serialize least-recent xterm scrollback, destroy it, create new for focused tab, reconnect PTY. Suspend/resume cycle < 50ms.

Project Accent Colors

Each project slot gets a distinct Catppuccin accent color for visual distinction:

Slot Color CSS Variable
1 Blue var(--ctp-blue)
2 Green var(--ctp-green)
3 Mauve var(--ctp-mauve)
4 Peach var(--ctp-peach)
5 Pink var(--ctp-pink)

Applied to border tint and header accent via var(--accent) CSS custom property set per ProjectBox.


Keyboard Shortcuts

Three-layer shortcut system prevents conflicts between terminal input, workspace navigation, and app-level commands:

Shortcut Action Layer
Ctrl+K Command palette App
Ctrl+G Switch group (palette filtered) App
Ctrl+1..5 Focus project by index App
Alt+1..4 Switch sidebar tab + open drawer App
Ctrl+B Toggle sidebar open/closed App
Ctrl+, Toggle settings panel App
Escape Close sidebar drawer App
Ctrl+Shift+F FTS5 search overlay App
Ctrl+N New terminal in focused project Workspace
Ctrl+Shift+N New agent query Workspace
Ctrl+Tab Next terminal tab Project
Ctrl+W Close terminal tab Project
Ctrl+Shift+C/V Copy/paste in terminal Terminal

Terminal layer captures raw keys only when focused. App layer has highest priority.


Key Constraints

  1. WebKit2GTK has no WebGL — xterm.js must use the Canvas addon explicitly. Maximum 4 active xterm.js instances to avoid OOM.

  2. Svelte 5 runes require .svelte.ts — Store files using $state/$derived must have the .svelte.ts extension. The compiler silently accepts .ts but runes fail at runtime.

  3. Single shared sidecar — All agent sessions share one SidecarManager. Per-project isolation is via cwd, claude_config_dir, and session_id routing. Per-project sidecar pools deferred to v3.1.

  4. SQLite WAL mode — Both databases use WAL with 5s busy_timeout for concurrent access from Rust backend + Python CLIs (btmsg/bttask).

  5. camelCase wire format — Rust uses #[serde(rename_all = "camelCase")]. TypeScript interfaces must match. This was a source of bugs during development (see findings.md for context).