Files
waggle-os/docs/backend-map/sections/03f-api-realtime-ops.md
Oleg Maslov 0c3e2ead3b
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled
moving
2026-09-02 10:10:29 +02:00

22 KiB

03f — Real-Time & Ops API Surface

Purpose. This section is the frontend rebuild contract for Waggle OS's real-time and operations backend: the two WaggleDance signal systems (the UI-facing /api/waggle/* stream plus the v2 protocol bus /api/waggle-dance/*), the audit /api/events stream, cron schedules, persisted notifications, offline message queue, encrypted backup/restore, the agent fleet, the LiteLLM / local-inference / Anthropic LLM proxies, filesystem browse, the browser extension health check, and Telegram outbound push. Four of these endpoints are Server-Sent Events (SSE) streams the UI subscribes to with EventSource; everything else is plain JSON REST.

All routes are served by the Node.js Fastify sidecar bundled into the Tauri desktop binary. Every path below was read directly from packages/server/src/local/routes/ and packages/server/src/local/.


1. SSE / streaming endpoints (read these first)

Four endpoints hold the connection open and push text/event-stream frames. The frontend consumes them with new EventSource(url). Each sends an initial connect frame, a periodic heartbeat/keepalive comment (: ...), and named events.

Endpoint Initial frame Named events emitted Heartbeat Source
GET /api/waggle/stream event: connected\ndata: {} signal (full WaggleSignal JSON) : heartbeat every 30s routes/waggle-signals.ts
GET /api/events/stream data: {"type":"connected"} audit (full AuditEvent JSON) : keepalive every 30s routes/events.ts
GET /api/notifications/stream data: {"type":"connected"} notification (default unnamed data: frame), subagent_status, workflow_suggestion : heartbeat every 30s routes/notifications.ts
POST /v1/chat/completions (when stream:true) none streamed OpenAI-format data: chunks, terminated by data: [DONE] none routes/anthropic-proxy.ts

Notes for the frontend:

  • On /api/notifications/stream, notification events arrive as unnamed data: frames (use eventSource.onmessage); subagent_status and workflow_suggestion arrive as named events (use addEventListener('subagent_status', ...)).
  • The SSE streams set Access-Control-Allow-Origin only when the request Origin passes the same exact-match allowlist the CORS plugin uses (corsOriginAllowed / validateOrigin). A cross-origin page cannot read these streams.
  • The connection is held open server-side via await new Promise(() => {}) (waggle/stream) or by hijacking the raw reply; closing the EventSource triggers request.raw.on('close') cleanup that removes the listener and clears the heartbeat.

2. WaggleDance signals — UI stream (/api/waggle/*)

Source: routes/waggle-signals.ts. In-memory store of the last 500 signals (newest-first unshift, capped). This is the legacy/high-level signal shape the existing WaggleDanceApp UI renders.

WaggleSignal shape (the JSON the UI receives)

Field Type Notes
id string sig-<epochMs>-<rand4>
type string e.g. agent:started, tool:called, memory:saved, agent:completed, agent:spawned, agent:error, or bridged waggle-dance:<category>
workspaceId string defaults to 'global' if unset
content string human-readable primary text
metadata Record<string,unknown> (optional) arbitrary provenance
timestamp string ISO-8601
acknowledged boolean toggled by the ack PATCH

Endpoints

Method Path Purpose
GET /api/waggle/signals List recent signals. Query: limit (default 50, max 200), unacked=1 (only unacknowledged). Returns { signals, total }.
POST /api/waggle/signals Publish a signal. Body { type, content, workspaceId?, metadata? } (type + content required). Returns 201 with the full signal.
PATCH /api/waggle/signals/:id/ack Mark a signal acknowledged. Returns { acknowledged: true, id }.
GET /api/waggle/stream SSE. Stream of new signals (see §1).

emitWaggleSignal(...) is the internal publish helper called from the chat loop and fleet.ts spawn flow.


3. WaggleDance v2 protocol bus (/api/waggle-dance/*)

Source: routes/waggle-dance.ts + signal-bus.ts + waggle-dance-bridge.ts. This is the AI-OS Phase 1B cross-tool activity bus. It uses the protocol message shape (WaggleMessage), not the UI signal shape. A SignalBus ring buffer (default capacity 500, drops oldest) backs it, decorated onto the Fastify instance as server.signalBus.

WaggleMessage shape (packages/shared/src/types.ts)

Field Type Notes
id string auto-filled randomUUID()
teamId string defaults to personal::<senderId> when teamId omitted — this is the personal-tier moat
senderId string defaults to 'local'
type 'broadcast' | 'request' | 'response'
subtype MessageSubtype (10 values, see below)
content Record<string, unknown>
referenceId string | null
routing Array<{ userId; reason }> | null
createdAt Date auto-filled

Valid typesubtype combinations (packages/waggle-dance/src/protocol.ts)

The POST endpoint rejects (400) any combo not in this table:

type Allowed subtype values
request knowledge_check, task_delegation, skill_request, model_recommendation
response knowledge_match, task_claim
broadcast discovery, routed_share, skill_share, model_recipe

Endpoints

Method Path Purpose
POST /api/waggle-dance/signal Normalize + validate + dispatch a v2 signal. Body { type, subtype, content, senderId?, teamId?, referenceId?, routing? }. Returns 201 { dispatched: true, response, message }; 400 on validation/combo/dispatch failure.
GET /api/waggle-dance/signals Snapshot of the ring buffer, newest-first. Query: subtype?, tool?, teamId?, limit? (max 1000), since? (ISO). Returns { signals, total }.

The bridge (zero-frontend-change cross-tool activity)

installWaggleDanceBridge(bus) subscribes the v2 bus and re-emits every message into the legacy /api/waggle/signals stream as a waggle-dance:<category> signal, so the existing UI surfaces cross-tool activity with no changes. The 10 protocol subtypes collapse to 5 UI categories:

v2 subtype(s) → UI category
discovery, knowledge_check, skill_request discovery
task_delegation, skill_share, routed_share handoff
knowledge_match insight
task_claim, model_recipe, model_recommendation coordination
any with content.priority === 'critical' alert (overrides the above)

Bridged signal metadata preserves { subtype, senderId, tool, teamId, referenceId, routing, priority, protocolMessage }.


4. Audit events (/api/events)

Source: routes/events.ts. Backed by a separate audit.db SQLite database in dataDir (WAL mode), table audit_events. Default retention 90 days (cron cleanup). This is the full audit trail for tool calls, memory ops, workspace changes, approvals, and exports.

AuditEventType (15 values)

tool_call, tool_result, memory_write, memory_delete, workspace_create, workspace_update, workspace_delete, session_start, session_end, approval_requested, approval_granted, approval_denied, approval_auto, export, cron_trigger, data_erase_requested.

AuditEvent shape (camelCased in responses by normalizeEvent)

id, timestamp (ISO), workspaceId, userId?, eventType, toolName?, input? (parsed JSON), output? (parsed JSON), model?, tokensUsed?, cost?, sessionId?, approved? (boolean).

Endpoints

Method Path Purpose
GET /api/events Paginated, filterable listing. Query: workspaceId/workspace, type/eventType, from, to, sessionId, limit (default 100, max 1000), offset. Returns { events, total, limit, offset, hasMore, page, totalPages }.
GET /api/events/stats Aggregates. Query: workspaceId/workspace, days (default 30, max 365). Returns { totalEvents, period:{days,since}, byType, byDay, topTools }.
GET /api/events/stream SSE. Live audit events (see §1).

5. Cron schedules (/api/cron)

Source: routes/cron.ts, backed by server.cronStore (Wave 1.1 Solo Cron Service). Execution runs through server.scheduler.executeJob.

Schedule response shape (camelCased from DB snake_case)

id (number), name, cronExpr, jobType (CronJobType), jobConfig (object; corrupt rows degrade to {}), workspaceId, enabled (boolean), lastRunAt, nextRunAt, createdAt.

Endpoints

Method Path Purpose
POST /api/cron Create. Body { name, cronExpr, jobType, jobConfig?, workspaceId?, enabled? } (first three required). workspaceId:'global' is normalized to '*'.
GET /api/cron List all. Returns { schedules, count }.
GET /api/cron/:id Get one. 404 if not found, 400 if id non-numeric.
PATCH /api/cron/:id Update. Body any of { name, cronExpr, jobConfig, workspaceId, enabled }.
DELETE /api/cron/:id Delete. Returns { ok: true, id }.
POST /api/cron/:id/trigger Manually run now. Auto-enables a disabled job before executing. Returns { triggered, id, nextRunAt, autoEnabled, schedule } and emits a cron-category notification on success.
GET /api/cron/:id/history (registered in notifications.ts) Execution history. Query limit (default 20). Returns { history, count }. 503 if unavailable.

6. Notifications (/api/notifications)

Source: routes/notifications.ts. Two surfaces: the live SSE stream and a persisted-notification REST store (cronStore persists them so they survive restart).

NotificationEvent shape

type:'notification', title, body, category ('cron' \| 'approval' \| 'task' \| 'message' \| 'agent'), timestamp (ISO), actionUrl?.

The stream also relays two other event shapes:

  • subagent_status{ type, workspaceId, agents:[{ id, name, role, status:'pending'|'running'|'done'|'failed', task, toolsUsed, startedAt?, completedAt? }], timestamp }.
  • workflow_suggestion{ type, workspaceId, pattern:{ name, description, steps, tools, category }, reason, timestamp }.

Endpoints

Method Path Purpose
GET /api/notifications/stream SSE. Live notifications + subagent status + workflow suggestions (see §1).
GET /api/notifications List persisted notifications. Query since, limit (default 50), unread=true. Returns { notifications, count, unread }.
POST /api/notifications/:id/read Mark one read. Returns { read: true, id }.
GET /api/notifications/history Alias of list with limit default 100. Returns { notifications, count, unread }.
PATCH /api/notifications/:id/read Mark one read (PATCH variant).
POST /api/notifications/read-all Mark all read. Returns { markedRead: count }.

7. Offline mode (/api/offline)

Source: routes/offline.ts, backed by server.offlineManager. Queues user messages while the connection is down. If the manager is absent, status returns { offline:false, since:null, queuedMessages:0, lastCheck } and queue ops return 503/empty.

Method Path Purpose
GET /api/offline/status Current offline state { ...mgr.state, lastCheck }.
POST /api/offline/queue Queue a message. Body { message, workspaceId?/workspace? } (message required; workspace defaults 'default'). Returns { queued }.
GET /api/offline/queue List queued messages { messages }.
DELETE /api/offline/queue/:id Remove one. 404 if not found. Returns { removed: true }.
DELETE /api/offline/queue Clear all. Returns { cleared: <n> }.

8. Backup & restore (/api/backup, /api/restore)

Source: routes/backup.ts. Produces a single encrypted archive of the ~/.waggle/ data dir for machine migration. Format: gzipped JSON manifest, AES-256-GCM encrypted when a .vault-key exists, with magic header WAGGLE-BACKUP-V1. Extension .waggle-backup. Max 500 MB; excludes node_modules, .git, models (re-downloadable ONNX weights), and marketplace.db*.

Method Path Purpose
POST /api/backup Build + stream the archive as application/octet-stream (Content-Disposition: attachment; filename="waggle-backup-<date>.waggle-backup"). Response headers X-Waggle-Backup-Encrypted and X-Waggle-Backup-Files. 413 if over 500 MB; 400 if no files.
POST /api/restore Restore from an archive. Body { backup: <base64>, preview? }. preview:true returns { preview, backupCreatedAt, totalFiles, existingFiles, newFiles, conflicts } without writing. Apply returns { restored, filesRestored, totalFiles, conflicts, errors?, backupCreatedAt }. Path-traversal protected; marketplace.db is skipped (re-syncs on startup).
GET /api/backup/metadata Last backup info { lastBackupAt, sizeBytes, fileCount }. 404 if none.

9. Agent fleet (/api/fleet)

Source: routes/fleet.ts. Mission-Control surface over server.sessionManager workspace sessions. Spawning is free for all tiers (agents generate memory). maxSessions is tier-gated: FREE=3, PRO=10, TEAMS=25, ENTERPRISE/TRIAL=100.

Session shape (in GET /api/fleet)

workspaceId, workspaceName, personaId, model, status, lastActivity, durationMs, toolCount, tokensUsed, costEstimate.

Method Path Purpose
GET /api/fleet List active workspace sessions. Returns { sessions, count, maxSessions }.
POST /api/fleet/spawn Spawn a new agent session. Body { task, persona?, model?, parentWorkspaceId? } (task required). Creates/uses the workspace session, emits agent:spawned then fire-and-forget runs the agent loop emitting agent:started/tool:called/agent:completed/agent:error signals. Returns { id, workspaceId, sessionId, status, startedAt, task, persona, model }. 404 if workspace has no mind; 409 on spawn failure.
POST /api/fleet/:workspaceId/pause Pause a session. 404 if not found/already paused.
POST /api/fleet/:workspaceId/resume Resume a paused session. 404 if not found/not paused.
POST /api/fleet/:workspaceId/kill Abort + close a session. 404 if not found.

10. LiteLLM control (/api/litellm)

Source: routes/litellm.ts. Manages the optional LiteLLM router process (lifecycle helpers getLiteLLMStatus/startLiteLLM/stopLiteLLM) and proxies its model list.

Method Path Purpose
GET /api/litellm/status { running, port, error? }.
POST /api/litellm/restart Stop then start. { running, port, error? }.
GET /api/litellm/models Available model IDs from LiteLLM { models: string[] } (empty array on failure).
GET /api/litellm/pricing Static per-model pricing array [{ model, inputPer1k, outputPer1k, provider }] (claude-sonnet/haiku/opus-4-6, gpt-5.4(+mini), gemini-3.1-pro/flash).

11. Local inference (/api/local-inference)

Source: routes/local-inference.ts. Hardware detection + local model recommendations via the llmfit CLI (falls back to OS/RAM basics), plus Ollama/vLLM availability and model pulls. Ollama defaults to http://localhost:11434, vLLM to http://localhost:8000 (overridable via OLLAMA_HOST/VLLM_HOST).

Method Path Purpose
GET /api/local-inference/hardware Detect GPU/RAM/CPU. `{ hardware:, source:'llmfit'
GET /api/local-inference/models Recommend models that fit. Query useCase?, limit? (default 20). { models:<ModelRecommendation[]>, source, totalScanned }.
GET /api/local-inference/status Ollama/vLLM availability + installed models. { servers, primaryServer, ollamaInstalled, ollamaUrl, vllmUrl, totalLocalModels }.
POST /api/local-inference/pull Pull a model via Ollama. Body { model }. 502 if Ollama unreachable; up to 10-min timeout.

HardwareInfo includes totalRamGb, availableRamGb, cpuCores, cpuName, platform, hasGpu, gpuName, gpuVramGb, gpuCount, gpus[], backend. ModelRecommendation includes name, provider, parameterCount, paramsB, useCase, category, fitLevel, score, scoreComponents{quality,speed,fit,context}, estimatedTps, memoryRequiredGb, memoryAvailableGb, utilizationPct, bestQuant, runMode, runtime, contextLength, isMoe, notes[].


12. Anthropic proxy (/v1/chat/completions)

Source: routes/anthropic-proxy.ts. A built-in OpenAI-compatible proxy backed by the Anthropic Messages API — replaces LiteLLM when calling Anthropic models directly. The API key is read from the vault first (key anthropic), then ANTHROPIC_API_KEY env, then config.json. Applies Anthropic prompt caching (system + rolling last-3-message window).

Method Path Purpose
GET /v1/health/liveliness Always { status: 'healthy' } (built-in, never down).
POST /v1/chat/completions OpenAI chat-completions body { model, messages, tools?, stream?, stream_options?, max_tokens?, temperature? }. Translates to/from Anthropic. SSE when stream:true (OpenAI-format chunks, terminated by data: [DONE]); otherwise a single OpenAI { choices, usage, model } JSON. 500 if no API key.

Model names are normalized in mapModel (strips provider prefix, dots→dashes; Haiku 4.6 typos map to the valid Haiku 4.5 snapshot; Sonnet/Opus 4.6 pass through as floating aliases).


13. Filesystem browse (/api/browse)

Source: routes/browse.ts. System-level directory browsing (not workspace-scoped) for the Create-Workspace dialog. Local-only — both endpoints reject external origins with 403 via isLocalRequest. On Windows, abstract root / returns the full drive list.

Method Path Purpose
GET /api/browse/local List directories. Query path (default /). Returns { entries:[{name,path,type:'directory'}], current }. Skips hidden + non-dir entries. 403/404/400/403(EACCES).
POST /api/browse/local/mkdir Create a directory (recursive). Body { path }. Returns 201 { name, path, type:'directory' }.

14. Browser extension health (/api/browser-ext)

Source: routes/browser-ext.ts. Single health check for the apps/browser-ext Chrome MV3 extension. Ingest/ask flows reuse /api/memory/frames and /api/chat.

Method Path Purpose
GET /api/browser-ext/health { ok: true, version: '0.1.0', activeWorkspace } so the extension can show "saving to: <workspace>".

15. Telegram outbound push (/api/telegram)

Source: routes/telegram.ts. One-way push from Waggle to a user's Telegram (no webhook receiver). Bot token + chat_id stored in the vault (telegram_bot_token, telegram_chat_id). URL is hard-coded to api.telegram.org (no SSRF). Text capped at 4096 chars. pushTelegramMessage(server, text) is the internal hook cron jobs call.

Method Path Purpose
GET /api/telegram/status { configured, hasToken, hasChatId }.
POST /api/telegram/config Save creds. Body { botToken?, chatId? }. Validates botToken against <id>:<secret> pattern and chatId as a signed integer string; 400 on bad format. Returns { ok: true }.
POST /api/telegram/test Send a "Waggle is connected" test message. 400 if not configured; 502 on Telegram error. Returns { ok, messageId }.
POST /api/telegram/send Send arbitrary text. Body { text, parseMode? } (text required, ≤4096). 400/502. Returns { ok, messageId }.

16. How it connects (data flow)

flowchart TD
  subgraph Agent["Agent runtime / chat loop / fleet spawn"]
    AL[runAgentLoop]
  end

  subgraph V2["WaggleDance v2 (protocol)"]
    POSTv2["POST /api/waggle-dance/signal"]
    DISP[WaggleDanceDispatcher]
    BUS[(SignalBus ring buffer 500)]
    GETv2["GET /api/waggle-dance/signals"]
    BRIDGE[installWaggleDanceBridge]
  end

  subgraph UI["WaggleDance UI stream (legacy)"]
    EMIT[emitWaggleSignal]
    STORE[(signals[] 500)]
    GETs["GET /api/waggle/signals"]
    SSEs["GET /api/waggle/stream (SSE)"]
  end

  subgraph Ops["Ops subsystems"]
    AUDIT[(audit.db SQLite)]
    SSEev["GET /api/events/stream (SSE)"]
    EB[(eventBus EventEmitter)]
    SSEnotif["GET /api/notifications/stream (SSE)"]
    CRON[cronStore + scheduler]
  end

  Frontend["Frontend (EventSource + fetch)"]

  AL -->|emitWaggleSignal| EMIT
  POSTv2 --> DISP --> BUS
  BUS --> GETv2
  BUS -->|subscribe| BRIDGE -->|waggle-dance:category| EMIT
  EMIT --> STORE --> GETs
  EMIT -->|EventEmitter signal| SSEs --> Frontend
  GETs --> Frontend
  GETv2 --> Frontend

  AL -->|emitAuditEvent| AUDIT
  AUDIT -->|eventBus audit_event| EB --> SSEev --> Frontend
  CRON -->|emitNotification| EB
  EB -->|notification / subagent_status / workflow_suggestion| SSEnotif --> Frontend
  CRON -->|saveNotification| AUDIT

Key wiring facts:

  • Two parallel signal systems exist: the legacy UI store (signals[], EventEmitter, /api/waggle/*) and the v2 protocol bus (SignalBus, /api/waggle-dance/*). The bridge is one-directional: v2 → legacy. The frontend only needs to read /api/waggle/stream to see both.
  • server.eventBus (a Node EventEmitter, max 50 listeners) is the shared relay for audit, notification, subagent-status, and workflow-suggestion SSE.
  • Persistence: audit events → audit.db; notifications + cron history → cronStore; v2 signals + UI signals are in-memory only (lost on sidecar restart).