Files
waggle-os/docs/backend-map/sections/05e-subsystem-waggledance-aios.md
Oleg Maslov 0c3e2ead3b
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled
moving
2026-09-02 10:10:29 +02:00

414 lines
28 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 05e — Subsystem: WaggleDance + the AI-OS Arc
**Purpose.** This subsystem is Waggle's multi-agent coordination layer. It has two halves that meet at one shared protocol (`WaggleMessage`): (1) **WaggleDance** — a message protocol + dispatcher + in-memory ring buffer (`SignalBus`) that carries "what's happening across agents/tools right now"; and (2) the **AI-OS arc** — the path that detects 7 external AI tools on the user's machine, installs reversible hooks into them, launches them with a workspace-context env var, then lets those tools' hooks emit signals back into Waggle so the UI shows cross-tool activity in real time. There is also a local sub-agent layer (spawn/list/result, orchestrated workflows, cross-workspace messaging) that shares the same vocabulary.
This file is the contract for rebuilding the frontend surfaces that consume these endpoints (the **WaggleDanceApp** activity feed, the **LauncherApp** dock, the **Mission Control** inventory tile, and the **Room** sub-agent tiles). Every identifier and path below is quoted from the source under `packages/waggle-dance/`, `packages/agent/src/`, `packages/hive-mind-shim-core/src/`, `packages/shared/src/`, and `packages/server/src/local/`.
---
## 1. Mental model — two signal planes
There are **two distinct signal shapes** in this subsystem, and a **bridge** that converts one into the other. Do not confuse them.
| Plane | Shape | Subtypes | Where it lives | Who consumes it |
|---|---|---|---|---|
| **v2 protocol bus** (WaggleDance) | `WaggleMessage` | 10 protocol subtypes (`discovery`, `task_delegation`, …) | `SignalBus` ring buffer (`packages/server/src/local/signal-bus.ts`) | `GET /api/waggle-dance/signals`; the bridge |
| **Legacy UX stream** | `WaggleSignal` | 5 UX categories (`discovery`/`handoff`/`insight`/`alert`/`coordination`) | in-memory array in `waggle-signals.ts` | `GET /api/waggle/signals` + `GET /api/waggle/stream` (SSE); the **WaggleDanceApp UI** |
The **bridge** (`installWaggleDanceBridge`, `waggle-dance-bridge.ts`) subscribes to the v2 bus and re-emits every v2 message as a legacy UX signal, so the existing UI surfaces cross-tool activity with **zero frontend changes**. A frontend rebuild can target *either* plane; the legacy stream is the one the current UI already speaks and is SSE-streamed.
```mermaid
flowchart LR
subgraph v2["v2 protocol plane (WaggleMessage)"]
POST["POST /api/waggle-dance/signal"]
DISP["WaggleDanceDispatcher.dispatch()"]
BUS["SignalBus ring buffer (cap 500)"]
GET2["GET /api/waggle-dance/signals"]
end
subgraph legacy["legacy UX plane (WaggleSignal)"]
EMIT["emitWaggleSignal()"]
ARR["in-memory array (cap 500)"]
GET1["GET /api/waggle/signals"]
SSE["GET /api/waggle/stream (SSE)"]
end
POST --> DISP --> BUS --> GET2
BUS -->|installWaggleDanceBridge subscribes| EMIT
EMIT --> ARR --> GET1
ARR --> SSE
SSE -->|consumed by| UI["WaggleDanceApp UI (useWaggleDance)"]
```
---
## 2. The WaggleDance protocol (`packages/waggle-dance` + `packages/shared/src/types.ts`)
### 2.1 `WaggleMessage` — canonical protocol envelope
Defined in `packages/shared/src/types.ts`. This is the v2 signal shape.
| Field | Type | Nullable | Meaning |
|---|---|---|---|
| `id` | `string` | no | UUID; server-assigned (`randomUUID()`) on POST |
| `teamId` | `string` | no | Team id; defaults to `personal::<senderId>` when omitted (the personal-tier moat) |
| `senderId` | `string` | no | Logical sender (`'claude-code-hook'`, `'cursor-hook'`, `'local'`, `agent-loop:<persona>`) |
| `type` | `MessageType` | no | One of `'broadcast' \| 'request' \| 'response'` |
| `subtype` | `MessageSubtype` | no | One of 10 subtypes (see 2.2) |
| `content` | `Record<string, unknown>` | no | Free-form structured payload; preserved verbatim |
| `referenceId` | `string \| null` | yes | For `response` types: correlates back to a prior request |
| `routing` | `Array<{ userId: string; reason: string }> \| null` | yes | For `routed_share`: directed recipients + rationale |
| `createdAt` | `Date` | no | Server-assigned timestamp |
### 2.2 `MessageType` × `MessageSubtype` and valid combinations
`MessageType = 'broadcast' | 'request' | 'response'`
`MessageSubtype` (10 values):
`knowledge_check`, `task_delegation`, `skill_request`, `model_recommendation`, `knowledge_match`, `task_claim`, `discovery`, `routed_share`, `skill_share`, `model_recipe`.
Valid combos are enforced by `validateMessageTypeCombo(type, subtype)` in `packages/waggle-dance/src/protocol.ts` against `VALID_COMBINATIONS`:
| `type` | Allowed `subtype`s |
|---|---|
| `request` | `knowledge_check`, `task_delegation`, `skill_request`, `model_recommendation` |
| `response` | `knowledge_match`, `task_claim` |
| `broadcast` | `discovery`, `routed_share`, `skill_share`, `model_recipe` |
`isRoutedMessage(subtype)` returns true only for `routed_share`. An invalid combo at the route returns HTTP 400.
### 2.3 The dispatcher — `WaggleDanceDispatcher` (`dispatcher.ts`)
`dispatch(message: WaggleMessage): Promise<DispatchResult>` validates the combo, then branches on `subtype`. `DispatchResult = { handled: boolean; response?: string; error?: string }`.
Dependencies injected via `DispatchDeps`**v1** (team-internal) deps are required; **v2** (cross-tool bus) deps are all optional, so the route layer can wire them incrementally and unwired branches still return `handled: true`:
| Dep | Signature | Used by subtypes |
|---|---|---|
| `searchMemory` | `(query) => Promise<string>` | `knowledge_check` |
| `resolveCapability` | `(query) => Array<{source,name,description,available}>` | `skill_request` |
| `spawnWorker` | `(task, role, context?) => Promise<string>` | `task_delegation` |
| `emitSignal?` | `(msg) => Promise<void>` | `discovery`, `routed_share`, `model_recipe` (broadcasts) |
| `recordResponse?` | `(msg) => Promise<void>` | `knowledge_match`, `task_claim` (responses) |
| `recommendModel?` | `(query, context) => Promise<string \| null>` | `model_recommendation` |
**Per-subtype dispatch behavior (the 6 v2 branches the prompt asks about plus the 4 v1):**
| Subtype | Handler | Behavior | Required `content` fields |
|---|---|---|---|
| `task_delegation` (v1) | `handleTaskDelegation` | Calls `spawnWorker(task, role, context)` | `content.task` (required); `content.role` (default `'analyst'`), `content.context` |
| `knowledge_check` (v1) | `handleKnowledgeCheck` | Calls `searchMemory(query)` | `content.query` or `content.topic` |
| `skill_request` (v1) | `handleSkillRequest` | Calls `resolveCapability(query)`, returns a formatted route list | `content.skill` or `content.query` |
| `skill_share` (v1) | `handleSkillShare` | Returns JSON `{action:'install_shared_skill', skillName, skillContent, sharedBy}` | `content.name`/`content.skill` + `content.content` |
| `discovery` (v2) | `handleBroadcastSignal` | If `emitSignal` set → emit to bus; else accept | none enforced |
| `routed_share` (v2) | `handleBroadcastSignal` | Same; UI shows as a directed handoff | `routing` carries recipients |
| `model_recipe` (v2) | `handleBroadcastSignal` | Same; shares a model+prompt recipe | none enforced |
| `knowledge_match` (v2) | `handleResponseRelay` | If `recordResponse` set → record; correlated via `referenceId` | none enforced |
| `task_claim` (v2) | `handleResponseRelay` | Same; a worker claiming a delegated task | none enforced |
| `model_recommendation` (v2) | `handleModelRecommendation` | Calls `recommendModel(query, restOfContent)`; friendly fallback if unwired | `content.query` (required) |
`hive-query.ts` defines `HiveQuery { topic; scope? }` and `HiveQueryResult { entities[]; relatedTasks[]; relatedMessages[] }` — DB-backed hive queries are executed by the server's `MessageService`, not in this package (the package is types + dispatcher only). `packages/waggle-dance/src/index.ts` barrel-exports `protocol.js`, `hive-query.js`, `dispatcher.js`.
---
## 3. `SignalBus` — the v2 ring buffer (`packages/server/src/local/signal-bus.ts`)
In-memory ring buffer of `WaggleMessage`. Created once per server, decorated as `server.signalBus`. **Not durable** — survives until sidecar restart or rollover.
- `DEFAULT_BUFFER_SIZE = 500`. On overflow the **oldest** is dropped (`shift()`).
- `record(signal)` → appends, drops oldest if over capacity, notifies all subscribers synchronously (subscriber errors are swallowed so one bad subscriber can't poison the bus), returns the signal.
- `query(filter)` → snapshot, **newest first**. `SignalFilter`:
| Filter field | Type | Effect |
|---|---|---|
| `subtype` | `MessageSubtype` | exact match |
| `tool` | `string` | matches `content.tool` |
| `teamId` | `string` | exact match |
| `since` | ISO string | `createdAt > since` |
| `limit` | number | cap result count |
- `subscribe(sub)` → returns an unsubscribe fn (this is how the bridge attaches).
- `size` getter; `clear()` (tests).
---
## 4. The two streaming UX endpoints (legacy plane) — `waggle-signals.ts`
`WaggleSignal` (the legacy/UX shape the current UI renders):
| Field | Type | Nullable | Meaning |
|---|---|---|---|
| `id` | `string` | no | `sig-<ts>-<rand>` |
| `type` | `string` | no | e.g. `agent:started`, `tool:called`, `memory:saved`, `agent:completed`, or bridged `waggle-dance:<category>` |
| `workspaceId` | `string` | no | Defaults to `'global'` |
| `content` | `string` | no | Human-readable primary text |
| `metadata` | `Record<string,unknown>` | yes | Provenance (bridge fills `subtype`/`senderId`/`tool`/`teamId`/`referenceId`/`routing`/`priority`/`protocolMessage`) |
| `timestamp` | `string` (ISO) | no | Emit time |
| `acknowledged` | `boolean` | no | Ack state |
Store is a capped in-memory array (`MAX_SIGNALS = 500`, newest-first via `unshift`). `emitWaggleSignal(...)` is the exported publish fn used by the chat loop **and** the bridge.
---
## 5. The bridge — v2 → legacy (`waggle-dance-bridge.ts`)
`installWaggleDanceBridge(bus: SignalBus): () => void` subscribes to the v2 bus and re-emits each `WaggleMessage` via `emitWaggleSignal`. It is auto-installed the first time `server.signalBus` is created (inside `waggle-dance.ts`).
**Subtype → UX category mapping** (`categorizeSubtype`, exported for tests):
| v2 `subtype` | UX category | Legacy `type` emitted |
|---|---|---|
| `discovery`, `knowledge_check`, `skill_request` | `discovery` | `waggle-dance:discovery` |
| `task_delegation`, `skill_share`, `routed_share` | `handoff` | `waggle-dance:handoff` |
| `knowledge_match` | `insight` | `waggle-dance:insight` |
| `task_claim`, `model_recipe`, `model_recommendation` | `coordination` | `waggle-dance:coordination` |
| *(any subtype)* with `priority === 'critical'` | `alert` (override) | `waggle-dance:alert` |
Priority is resolved by `inferPriority`: `content.priority` (`low/normal/high/critical`) wins; else `content.importance` (`high`→high, `critical`→critical); else `normal`. `buildLegacyContent` builds the display string as `"<subtype>: <topic>"` where topic falls back through `content.topic → title → query → task → skill`, then to a key summary. Full provenance (including the verbatim `protocolMessage`) is carried in `metadata`.
---
## 6. AI-OS Phase 0 — tool detection (`packages/shared/src/tool-detection.ts` + `packages/agent/src/tool-detection.ts`)
### 6.1 The 7 supported tools
`SUPPORTED_TOOLS` (`@waggle/shared`), with display names from `TOOL_DISPLAY_NAMES`:
| `ToolId` | Display name | Detection strategy | Linux/macOS/Win paths |
|---|---|---|---|
| `claude-code` | Claude Code | PATH lookup (`claude`) | `which`/`where.exe` |
| `claude-desktop` | Claude Desktop | candidate paths | `/Applications/Claude.app/...`, `%LOCALAPPDATA%\AnthropicClaude\Claude.exe` |
| `cursor` | Cursor | candidate paths | `/Applications/Cursor.app/...`, `…\Programs\cursor\Cursor.exe` |
| `codex` | Codex CLI | PATH lookup (`codex`) | `which`/`where.exe` |
| `codex-desktop` | Codex Desktop | candidate paths (unreleased; speculative vendor paths) | `/Applications/Codex.app/...` |
| `hermes` | Hermes Agent | PATH lookup (`hermes`) | `which`/`where.exe` |
| `openclaw` | OpenClaw | PATH lookup (`openclaw`) | `which`/`where.exe` |
`LAUNCH_COHORT` = all 7. (The launcher's runtime guard message still says "claude-code, cursor, claude-desktop" — a stale Phase-2 string — but the actual array includes all 7.)
### 6.2 `DetectedTool` (per-tool result)
| Field | Type | Meaning |
|---|---|---|
| `id` | `ToolId` | tool id |
| `displayName` | `string` | from `TOOL_DISPLAY_NAMES` |
| `installed` | `boolean` | true iff binary found at a known path |
| `installedPath` | `string \| null` | absolute path to binary |
| `version` | `string \| null` | best-effort (`--version`); null if exec failed |
| `hooksInstalled` | `boolean` | true iff hook pointer file exists **and** its referenced `backup` file still exists (partial rollback → false) |
| `hookPointerPath` | `string \| null` | the pointer file probed (`~/<config-dir>/hive-mind-install.json`) |
| `diagnostic?` | `string` | optional human reason for a partial failure (e.g. `'--version exec failed'`) |
### 6.3 `ToolDetectionResult` (envelope) — returned by `GET /api/tools/detect`
| Field | Type | Meaning |
|---|---|---|
| `platform` | `NodeJS.Platform \| 'other'` | platform detection ran on |
| `detectedAt` | `string` (ISO) | completion timestamp |
| `tools` | `DetectedTool[]` | per-tool results, **in `SUPPORTED_TOOLS` order** (stable for the UI) |
Implementation notes: `detectInstalledTools(opts?)` runs every detector in parallel (`Promise.all`) but preserves `SUPPORTED_TOOLS` order in the output. All fs/exec/PATH calls are injectable (`ToolDetectionDeps`) for hermetic tests. Hook-pointer config dirs are in `HOOK_POINTER_BY_TOOL` (e.g. `claude-code``.claude/hive-mind-install.json`, `cursor``.cursor/...`, `codex`/`codex-desktop``.codex/...`).
---
## 7. AI-OS Phase 2 — launcher + hook installer (`packages/agent/src/tool-launcher.ts` + `tool-process-tracker.ts`)
### 7.1 `launchTool(opts) → LaunchResult`
Spawns the tool **detached** (`detached: true`, `stdio: 'ignore'`, `unref()` so it outlives the sidecar). Injects `WAGGLE_WORKSPACE_ID` into the child env when `workspaceId` is given — this is the single env var the tool's hooks pick up (via shim-core `workspace-resolver`) to tag captured memory.
`LaunchOptions`: `{ id: ToolId; installedPath: string (required); workspaceId?; cwd? (default = dirname of binary); args?; deps? }`.
`LaunchResult`: `{ ok: boolean; pid: number | null; executed: { binary; args; cwd? }; error? }`.
Guards: `id` must be in `LAUNCH_COHORT` (all 7); `installedPath` required (caller passes it from a fresh detect).
### 7.2 `runHookCommand(opts) → HookCommandResult` (reversible hooks)
Runs `npx --yes @waggle/hive-mind-hooks-<id> <install|verify|uninstall>` (adds `--cli-path <path>` only on `install`). Captures stdout/stderr/exit-code.
- `HookAction = 'install' | 'verify' | 'uninstall'`.
- **`HOOKS_COHORT`** = `['claude-code', 'codex', 'codex-desktop', 'cursor', 'hermes', 'openclaw']` (6 tools — every tool with a real `bin`). `claude-desktop` is excluded (binless stub). Hook actions gate on `HOOKS_COHORT`, **not** `LAUNCH_COHORT`, so the UI never offers a hook action npx can't fulfil.
- `hookPackageFor(id)``@waggle/hive-mind-hooks-${id}`.
- `HookCommandResult`: `{ ok; action; packageName; stdout; stderr; code; error? }`.
Reversibility: install writes a pointer file `~/<config>/hive-mind-install.json` whose `backup` field points at the original config; uninstall restores it; detection's `hooksInstalled` flips false if the backup is gone (partial rollback is honestly reported).
### 7.3 `ToolProcessTracker` — the 'Running' badge backend
In-memory map of `TrackedProcess { pid; toolId; startedAt; workspaceId? }`. **Not durable** (dropped on restart — losing attribution is preferred over reporting stale liveness).
| Method | Behavior |
|---|---|
| `register(pid, toolId, workspaceId?)` | record a freshly spawned pid (idempotent on pid) |
| `list()` | GC dead pids (`process.kill(pid,0)` liveness), return alive ones |
| `forget(pid)` | drop a pid |
| `kill(pid, gracefulTimeoutMs=3000)` | refuses untracked pids; SIGTERM → wait → SIGKILL escalation; returns `{ ok; pid; reason }` where reason ∈ `not-tracked` / `already-dead` / `sigterm-ok` / `sigkill-ok` / `sigterm-failed-sigkill-failed` |
---
## 8. AI-OS Phase 1D — the shim-core emitter (`packages/hive-mind-shim-core/src/signal-emitter.ts`)
This is the library a tool's hook calls to push a signal back into Waggle. **Fail-open by design**: any error (ENOTFOUND, ECONNREFUSED, non-2xx, parse) returns `null` and logs one stderr warning — the host AI tool's hook chain never sees a failure. Uses only Node 20 built-in `fetch`, 2-second default timeout.
URL resolution: `options.url > env.WAGGLE_SIDECAR_URL > http://127.0.0.1:3333`.
- `emitSignalToWaggleDance(opts: EmitSignalOptions): Promise<EmittedSignal | null>` — POSTs to `<url>/api/waggle-dance/signal`.
- `EmitSignalOptions`: `{ type: SignalType; subtype: SignalSubtype; content: Record<string,unknown>; senderId? (default 'hook'); teamId?; referenceId?; routing?; url?; timeoutMs? (2000); fetchImpl?; onWarn? }`.
- `SignalType` / `SignalSubtype` mirror the protocol enums exactly.
- `EmittedSignal` is the server-persisted message echoed back: `{ id; teamId; senderId; type; subtype; content; referenceId; routing; createdAt }`.
- `maybeEmitDiscovery(eventType, importance, payload, opts)` — convenience policy: emit a `broadcast`/`discovery` only when `importance` is `'high'|'critical'` **and** `eventType` is `'stop'|'pre-compact'`. Phase 1E wires the claude-code Stop hook to this (opt-in via `WAGGLE_SIGNAL_EMIT`).
Hook event vocabulary (`hook-event-types.ts`): `EventType` = `session-start | session-end | user-prompt-submit | pre-compact | stop | pre-tool-use | post-tool-use`; `ShimSource` = `claude-code | cursor | hermes | codex | opencode | openclaw`.
---
## 9. The end-to-end AI-OS flow: detect → launch → signal → UI
```mermaid
sequenceDiagram
participant UI as Web UI (LauncherApp / Mission Control)
participant SC as Sidecar (Fastify @ :3333)
participant DET as detectInstalledTools()
participant LT as launchTool()
participant EXT as External AI tool (Claude Code, …)
participant HOOK as Tool hook (+ shim-core)
participant BUS as SignalBus (v2)
participant BR as installWaggleDanceBridge
participant STREAM as /api/waggle/stream (SSE)
UI->>SC: GET /api/tools/detect
SC->>DET: run 7 detectors in parallel
DET-->>SC: ToolDetectionResult { tools[] }
SC-->>UI: 200 ToolDetectionResult
UI->>SC: POST /api/tools/hooks { id, action:'install' }
SC->>EXT: npx @waggle/hive-mind-hooks-<id> install (reversible)
SC-->>UI: 200 HookCommandResult
UI->>SC: POST /api/tools/launch { id, installedPath, workspaceId }
SC->>LT: spawn detached, env WAGGLE_WORKSPACE_ID
LT-->>SC: { ok, pid }
SC->>SC: toolProcessTracker.register(pid,...)
SC-->>UI: 202 LaunchResult { pid }
EXT->>HOOK: lifecycle event (stop / pre-compact)
HOOK->>SC: POST /api/waggle-dance/signal (emitSignalToWaggleDance, fail-open)
SC->>SC: WaggleDanceDispatcher.dispatch()
SC->>BUS: emitSignal → bus.record(WaggleMessage)
BUS->>BR: subscriber fires
BR->>SC: emitWaggleSignal(mapped WaggleSignal)
SC-->>STREAM: event: signal (SSE)
STREAM-->>UI: live cross-tool activity
```
Rollback anchor for the whole arc: git tag `checkpoint/pre-ai-os-2026-05-20`.
---
## 10. Local sub-agent + cross-workspace layer (same vocabulary, different transport)
These are **agent tools** (callable by the LLM), not HTTP routes, but the frontend sees their effects through the notifications/Room stream and they share the coordination theme.
### 10.1 Sub-agent spawning — `createSubAgentTools(deps)` (`subagent-tools.ts`)
Exposes **3 tools** to the main agent. Sub-agents run in-process via the shared `runLoop`. In-memory registries: `activeAgents` + `agentResults` (capped at `MAX_AGENT_RESULTS = 100`, stale eviction at `STALE_THRESHOLD_MS = 30 min`).
| Tool | Inputs | Output |
|---|---|---|
| `spawn_agent` | `name`, `role`, `task` (required); `context?`, `tools?` (role=`custom` only), `model?`, `max_turns?` (50) | Markdown sub-agent result (role, duration, tools used, tokens, content) |
| `list_agents` | none | Active + completed agents with previews |
| `get_agent_result` | `agent_id` (or name) | Full stored result |
`ROLE_TOOL_PRESETS` maps role → tool allowlist: `researcher`, `writer`, `coder`, `analyst`, `reviewer`, `planner` (default fallback = `analyst`). Lifecycle is surfaced to the UI via `onSubAgentStatus` (`running`/`done`/`error`) which the server wires to `emitSubagentStatus` → the Room canvas. `onSubAgentComplete` persists results to the active mind (best-effort, never throws).
`SubAgentResult`: `{ agentId; agentName; role; response; usage{inputTokens,outputTokens}; toolsUsed[]; duration; completedAt }`. `SubAgentStatusEvent`: `{ agentId; name; role; status; task; toolsUsed[]; startedAt; completedAt? }`.
### 10.2 Workflow orchestration — `SubagentOrchestrator` (`subagent-orchestrator.ts`)
Supervisor/worker pattern over `spawn_agent`. `runWorkflow(template)` executes steps in **dependency order** (topological, sequential), injects upstream results as context, then aggregates. Emits `worker:status` events (an `EventEmitter`).
- `WorkflowStep`: `{ name; role; task; tools?; dependsOn?; contextFrom?; maxTurns? }`.
- `WorkflowTemplate`: `{ name; description; steps[]; aggregation: 'concatenate' | 'last' | 'synthesize' }`.
- `WorkerState`: `{ id; name; role; status: 'pending'|'running'|'done'|'failed'; task; startedAt?; completedAt?; result?; error?; toolsUsed[]; usage }`.
- Adds `synthesizer` + `summarizer` to the role presets. Circular dependency → remaining steps marked `failed` with a clear error.
### 10.3 Cross-workspace messaging — `AgentMessageBus` + `createAgentCommsTools`
`AgentMessageBus` (`agent-message-bus.ts`) is an **in-memory, per-machine** bus for concurrent agent sessions in different workspaces (distinct from team/PostgreSQL messaging). `AgentMessage`: `{ id; from (workspaceId); to (workspaceId); content; correlationId?; timestamp; ttlMs }`. Default TTL = 5 min; `receive()` is one-shot (drains + filters expired); `peek()` is non-destructive; `cleanup()` GCs expired.
`createAgentCommsTools(bus, currentWorkspaceId, isSessionActive?)` exposes **2 tools**:
| Tool | Inputs | Notes |
|---|---|---|
| `send_agent_message` | `workspace`, `message`, `correlationId?` | Rejects self-send; rejects target whose session isn't active |
| `check_agent_messages` | none | Consumes pending messages for the current workspace |
---
## 11. API reference — every route
All routes are served by the **local sidecar** (Fastify, loopback `:3333`). Registration in `packages/server/src/local/index.ts`: `toolsRoutes` (1990), `waggleDanceRoutes` (1991), `waggleSignalRoutes` (2016). The `signalBus` and `toolProcessTracker` decorations propagate to the parent instance via `fastify-plugin`.
| Method | Full path | Request shape | Response shape | Streaming? |
|---|---|---|---|---|
| POST | `/api/waggle-dance/signal` | `{ type, subtype, content, senderId?, teamId?, referenceId?, routing? }` (Zod `signalRequestSchema`) | `201 { dispatched:true, response, message: WaggleMessage }` · `400 { error, details? }` | No |
| GET | `/api/waggle-dance/signals` | query `{ subtype?, tool?, teamId?, limit?(≤1000), since? }` | `200 { signals: WaggleMessage[], total }` (newest-first) · `400` | No |
| GET | `/api/waggle/signals` | query `{ limit?(≤200, default 50), unacked?('1') }` | `{ signals: WaggleSignal[], total }` | No |
| POST | `/api/waggle/signals` | `{ type, content, workspaceId?, metadata? }` | `201 WaggleSignal` · `400 { error }` | No |
| PATCH | `/api/waggle/signals/:id/ack` | path `:id` | `{ acknowledged:true, id }` | No |
| GET | `/api/waggle/stream` | — (SSE; Origin echoed only if allow-listed) | `event: signal` frames of `WaggleSignal`; `:heartbeat` every 30s | **Yes (SSE)** |
| GET | `/api/tools/detect` | — | `200 ToolDetectionResult` · `500 { error, message }` | No |
| POST | `/api/tools/launch` | `{ id, installedPath, workspaceId?, cwd?, args? }` (Zod) | `202 LaunchResult` · `400 LaunchResult`/validation | No |
| GET | `/api/tools/processes` | — | `200 { processes: TrackedProcess[], total }` | No |
| POST | `/api/tools/kill` | `{ pid }` | `200 { ok, pid, reason }` · `404 not-tracked` · `500` | No |
| POST | `/api/tools/hooks` | `{ id, action:'install'\|'verify'\|'uninstall', cliPath? }` | `200 HookCommandResult` · `400` · `500 { error, message }` | No |
**Server-side defaulting on POST `/api/waggle-dance/signal`:** `id`=`randomUUID()`, `createdAt`=now, `senderId`=`'local'` if omitted, `teamId`=`personal::<senderId>` if omitted, `referenceId`/`routing`=`null`. The route wires real v2 deps (`emitSignal``bus.record`, `recordResponse``bus.record`, `recommendModel``null` stub). v1 deps are local stubs (the team-internal paths run through the cloud team workspace, not this loopback route).
---
## 12. Phase 3 — skill diffusion (closed-loop → bus)
When the agent's D1 closed learning loop fires (`onSkillDistillationFire`, wired in `chat.ts`), the server records a `skill_share` broadcast directly onto `server.signalBus`:
```
{ type:'broadcast', subtype:'skill_share',
teamId:'personal::<workspace>', senderId:'agent-loop:<persona>',
content:{ tool:'waggle-agent', patternKey, toolsUsed[], directive, sessionId, workspaceId } }
```
Via the bridge this surfaces in the UI as a `waggle-dance:handoff` signal, so MCP-consuming external tools can adopt the soon-to-be-authored skill. The whole emission is best-effort and gated on `server.signalBus` existing.
---
## 13. Frontend rebuild checklist
- **Activity feed (WaggleDanceApp):** consume `GET /api/waggle/stream` (SSE) for live signals and `GET /api/waggle/signals` for backfill; render by the 5 UX categories; ack via `PATCH /api/waggle/signals/:id/ack`. Drill-down detail is in `metadata.protocolMessage` (the raw `WaggleMessage`). If you want the raw protocol plane instead, poll `GET /api/waggle-dance/signals`.
- **Launcher dock (LauncherApp):** `GET /api/tools/detect` to list the 7 tools (installed/version/hooksInstalled); `POST /api/tools/hooks` for install/verify/uninstall (only enable for the 6 `HOOKS_COHORT` tools; `claude-desktop` has no hook action); `POST /api/tools/launch` (pass `installedPath` from the detect result + the current `workspaceId`); poll `GET /api/tools/processes` for the 'Running' badge; `POST /api/tools/kill` for stop.
- **Mission Control inventory tile:** the same `ToolDetectionResult` (count installed, count hooked).
- **Room sub-agent tiles:** driven by the notifications stream (`subagent_status`), fed by `onSubAgentStatus`/`emitSubagentStatus`; the underlying tools are `spawn_agent`/`list_agents`/`get_agent_result`.
---
## 14. Source map
| Concern | File |
|---|---|
| Protocol types | `packages/shared/src/types.ts` (`WaggleMessage`, `MessageType`, `MessageSubtype`) |
| Combo validation | `packages/waggle-dance/src/protocol.ts` |
| Dispatcher | `packages/waggle-dance/src/dispatcher.ts` |
| Hive-query types | `packages/waggle-dance/src/hive-query.ts` |
| v2 ring buffer | `packages/server/src/local/signal-bus.ts` |
| v2 routes | `packages/server/src/local/routes/waggle-dance.ts` |
| Bridge v2→legacy | `packages/server/src/local/waggle-dance-bridge.ts` |
| Legacy stream + SSE | `packages/server/src/local/routes/waggle-signals.ts` |
| Tool-detection types | `packages/shared/src/tool-detection.ts` |
| Tool-detection impl | `packages/agent/src/tool-detection.ts` |
| Launcher + hooks | `packages/agent/src/tool-launcher.ts` |
| Process tracker | `packages/agent/src/tool-process-tracker.ts` |
| Tools routes | `packages/server/src/local/routes/tools.ts` |
| Shim-core emitter | `packages/hive-mind-shim-core/src/signal-emitter.ts` |
| Hook event vocab | `packages/hive-mind-shim-core/src/hook-event-types.ts` |
| Sub-agent tools | `packages/agent/src/subagent-tools.ts` |
| Orchestrator | `packages/agent/src/subagent-orchestrator.ts` |
| Cross-workspace bus | `packages/agent/src/agent-message-bus.ts` + `agent-comms-tools.ts` |
| Skill-diffusion wiring | `packages/server/src/local/routes/chat.ts` (`onSkillDistillationFire`) |