live-qcontrol-stream
Live qcontrol event stream → qmap visualization via SSE bridge
New in this cycle: Live qcontrol stream — SSE bridge, MCP actors, tool-call flows from the extended reducer
Why
QControl now emits MCP protocol events (request/response/error/notification/session lifecycle) alongside the IO events (file/exec/connection/http) that c22 already handles. These MCP events are fully implemented and documented but invisible to qmap — the c22 reducer ignores them. Meanwhile, the c22 bridge serves reduced sessions via HTTP polling only, meaning the experiment must repeatedly fetch to see updates.
This cycle proves the live pipeline: qcontrol discover --json → bridge → SSE → qmap, with the reducer extended to surface MCP server actors and tool-call flows alongside the existing IO actors.
Adjacent cycles:
c22.runtime-session-map— proved the bridge + reducer pattern for IO events (file, exec, connection, http)c24.synthetic-live-session-replay— proved the replay/subscribe UX with synthetic data
Objective
One runnable experiment that:
- Connects to a live qcontrol session bridge via SSE (Server-Sent Events)
- Renders MCP servers, tools, files, external hosts, and processes as they appear in real time
- Uses an incremental session accumulator (O(1) per event) instead of full re-reduction
- Supports fixture mode for offline testing without a running bridge
- Supports polling fallback when SSE is unavailable
Scope
dev/experiments/c28.live-qcontrol-stream/— experiment surface, reducer, SSE source, bridgecycles/c28.live-qcontrol-stream/— cycle metadata
Architecture
Accumulator pattern
The c22 reducer replays all accumulated events on every call — O(n) per event. c28 restructures the same logic into createSessionAccumulator():
ingest(event)— O(1) per event (map lookup + update)snapshot()— O(n actors) on demand, only when the bridge needs to broadcast
The pure function reduceQControlSession(events) is preserved as a convenience wrapper for fixtures and tests.
Event flow
qcontrol discover --json (JSONL on stdin)
→ bridge.mjs
→ accumulator.ingest(event) O(1) per event
→ 250ms debounce fires:
→ accumulator.snapshot() O(n actors)
→ broadcast via SSE to all clients
→ browser EventSource
→ QMapAgentSession re-renders
Event coverage
| Event type | Actor type | Entity type | Notes |
|---|---|---|---|
mcp.request | mcp-server | MCP Server | |
mcp.request (tools/call) | skill | Tool | |
mcp.response | (existing server) | — | |
mcp.error | (existing server) | — | Notable event |
mcp.notification | (existing server) | — | Light volume |
mcp.session_close | (existing server) | — | Tags closed |
mcp.diagnostic | (existing server) | — | Notable event |
mcp.oauth | (existing server) | — | Notable on error |
llm.request | vendor | LLM Provider | New |
llm.response | (existing provider) | — | Token/cost stats, notable on error |
llm.usage | (existing provider) | — | Context window metadata |
llm.rate_limit | (existing provider) | — | Notable when actively limited |
agent.tool_call (mcp) | skill | Tool | Bridges to MCP tool tracking |
agent.tool_call (skill) | skill | Skill | New actor |
agent.tool_call (builtin) | — | — | Stats on root actor |
agent.tool_decision | — | — | Notable on denial |
agent.tool_result | — | — | Notable on failure |
process.snapshot | — | — | Bootstraps root actor |
IO events (file.read/write, exec.spawn, connection.*, http.request) carry forward from c22 unchanged.
Non-goals
agent.message— conversation turns (no actor/flow mapping; future timeline feature)file.open/close,connection.close,sse.*,websocket.*— lifecycle bookends already covered by higher-level events- Production-grade error recovery or reconnection UX
- Promoting anything to
src/— this stays experiment-local - Real-time session switching (single-session focus is fine)
- Diff-aware SSE (full snapshots are fine at current scale)
Progress
2026-05-13: Pipeline working, MCP events flowing
Bridge + discover pipeline:
- Fixed event format mismatch: discover outputs
{ type, payload, process }(noeventwrapper field). Updated bothbridge.mjsingestEvent()and accumulatoringest()to accept both legacy (event.event === "process.log") and current (baretype) formats. - Fixed SSE session flicker: bridge was re-evaluating "newest session" on every broadcast, causing actors to appear/disappear as events alternated between PIDs. Fixed by locking SSE clients to a session at connect time.
- Upgraded to multi-session SSE: bridge now broadcasts all sessions (event:
"sessions") to unfiltered clients, single session (event:"snapshot") to?pid=clients. Frontend decides how to merge/group.
MCP event normalization:
- MCP payload schema changed: old format nested under
data.mcp, new format puts fields directly underpayloadwithsession: { host, transport }sub-object. Added normalizer iningest()that flattens both shapes into a singlemcpobject. - Required
--plugins core-tools(from qcontrolplugin-core-toolsbranch) plusinject_mcp_server = truein~/.config/qcontrol/run.toml.
Root actor labeling:
classifyRoot()now recognizes~/.local/share/claude/versions/<ver>path pattern → label becomes "Claude Code (2.1.107)" instead of raw version number.
QMapLiveStream wrapper component:
- New
src/components/QMapLiveStream.vue— takessessionsarray, merges actors/flows internally, deduplicates by ID, sums flow volumes. QMapLiveStreamDefaults.js— all dataflows highlighted, solid lines, distributed arrows (3 count, 2.5 size, 0.5 speed, 0.9 opacity).- Supports
groupBy: { level1: "session" }to rug actors by their agent session.
View modes:
- Entity Type: all sessions merged, grouped by entity type (Agent, MCP Server, External Host)
- Session: each agent session gets its own rug with entity type subdivisions
- Flat: no grouping
- Defaults to Live SSE + Session view.
qcontrol setup (for reference)
# Build from plugin-core-tools branch
cd ~/web/qpoint/qcontrol
git checkout plugin-core-tools
make
# Configure
# ~/.config/qcontrol/run.toml:
# [features]
# inject_mcp_server = true
# Claude shim (~/.local/bin/claude) points to local build:
# __qc_qctrl=/Users/mark/web/qpoint/qcontrol/bin/qcontrol
# exec "$__qc_qctrl" run --plugins io-events,core-tools -- "$__qc_target" "$@"
# Pipeline
~/web/qpoint/qcontrol/bin/qcontrol discover --socket --json 2>/dev/null | node dev/experiments/c28.live-qcontrol-stream/bridge/bridge.mjs
Success Criteria
- Live pipeline works end-to-end:
qcontrol discover --json | node bridge.mjs→ browser shows actors appearing in real time - MCP server and tool actors appear with correct types, labels, and flows
- Fixture mode loads and renders without a running bridge
- Bridge SSE endpoint delivers snapshot-on-connect + debounced updates
- Code is generously commented for cross-project readability
Visual record
9 captures — heroes, page slices, and full-page renders. Click to view.








