Skip to content

[Protocol] Execution Web Event Schema for Tier 1 visualization #439

Description

@xinhuagu

Overview

Define a canonical execution event schema for browser clients consuming AceClaw runtime state.

This issue is the protocol foundation for #430. It must be completed before the WebSocket bridge or React reducer are treated as stable.

Why

AceClaw already emits CLI-oriented streaming notifications (stream.text, stream.tool_use, stream.tool_completed, permission.request, stream.plan_*, etc.), but browser clients need a stable event model that can:

  • reconstruct an in-flight execution tree
  • recover after refresh/reconnect
  • correlate tool calls, permission pauses, and future plan/subagent nodes
  • evolve without forcing UI heuristics into the reducer

The goal is not to replace existing daemon events. The goal is to define an additive, browser-facing contract.


Key Architectural Decision: Reducer Builds the Tree (Not Schema)

Decision: Schema layer defines flat wire format (= daemon events as-is). Reducer layer (#435) does all tree mapping.

Layer Responsibility Does NOT do
Schema (this issue) TypeScript types for daemon JSON-RPC notifications (flat) Tree structure, parent-child, parallel detection
Reducer (#435) Map flat events → ExecutionNode tree Wire format transformation

Why Reducer, Not Schema (Detailed Rationale)

1. Separation of Concerns

Daemon's job = execute agent loop + report WHAT HAPPENED (facts)
Frontend's job = decide HOW TO DISPLAY IT (presentation)
  • If daemon does tree mapping: frontend UX changes → daemon must change → tight coupling
  • If reducer does tree mapping: frontend evolves independently → zero daemon changes
  • The tree structure IS a presentation concern (how to nest, when to fold, what to group)

2. Pure Function vs Multi-Thread State

Reducer (frontend) Daemon tree (hypothetical)
(oldState, event) => newState Multiple Virtual Threads producing events concurrently
Pure function, zero side effects ConcurrentHashMap + locks/CAS
Unit testable (give input, check output) Integration test only (need running daemon)
Redux DevTools time-travel debugging No equivalent debugging tool
Single-threaded (JS event loop) Race conditions possible

3. Multi-Client Future

"One kernel. Many windows." (PRD-Frontend.md §2)

Future clients need DIFFERENT tree shapes from same events:
  - CLI          → no tree, just text stream
  - Web UI       → deep nested tree with animations
  - VS Code      → TreeView API format (different structure)
  - Mobile       → simplified 2-level tree
  - Slack bot    → plain text summary

If daemon sends flat events: each client reduces/transforms independently → perfect
If daemon sends tree: all clients get same shape → mismatch for most

4. Parallel Detection MUST Be in Frontend

Daemon sends (time order):
  t=0ms   stream.tool_use(id="t1", name="read_file")
  t=2ms   stream.tool_use(id="t2", name="grep")        ← starts while t1 running
  t=100ms stream.tool_completed(id="t1", duration=100)
  t=200ms stream.tool_completed(id="t2", duration=198)

Reducer logic:
  "t2 arrived while t1 status is still 'running' → they are parallel"
  → set parallel=true on both nodes

If daemon had to do this:
  Thread executing t1 and thread executing t2 are different VTs
  Daemon would need: "before sending tool_use for t2, check if t1 completed"
  → cross-thread state query → race condition → complex and fragile

Reducer does this trivially because events arrive sequentially in JS.

5. Network Efficiency

Flat events (chosen) Tree objects (rejected)
~100 bytes per event Full tree snapshot or JSON diff
Stream as they happen Batch or compute diff
O(1) per event O(n) where n = tree size
WebSocket text frame Needs custom diff protocol

Implication for this deliverable


Missing Events — Daemon Must Add

Critical discovery: Some events exist only in EventBus (internal) and are never sent as JSON-RPC notifications to any client.

Missing Event Priority Current State Required Change
stream.turn_started HIGH TurnStarted only in EventBus (metrics/journal) ~5 lines in StreamingAgentHandler.java
stream.turn_completed HIGH TurnCompleted only in EventBus (metrics/journal) ~5 lines in StreamingAgentHandler.java
stream.session_started MEDIUM SessionEvent.Created in EventBus only ~5 lines when session is created
stream.plan_step_fallback LOW Fallback triggers but no notification emitted ~5 lines in SequentialPlanExecutor.java

Impact: Without stream.turn_started/turn_completed, frontend CANNOT draw Turn nodes. It would have to use fragile heuristics (infer turn boundaries from tool_use timing gaps). Adding these is ~10 lines total.

This issue should:

  1. Define the params schema for these new notifications
  2. Document them as part of the wire format
  3. Mark feat(daemon): WebSocket endpoint + EventMultiplexer for real-time event streaming #431 as needing to emit them

Non-Negotiable Constraints

  • Existing JSON-RPC over UDS remains unchanged
  • Existing CLI streaming remains unchanged
  • Existing daemon runtime authority remains unchanged
  • Web event schema is additive and derived from daemon runtime state
  • Do NOT reshape existing events to be tree-aware

Deliverables

1. TypeScript wire-format types (DaemonEvent union)

// Wire format — mirrors daemon exactly. FLAT, no tree structure.
type DaemonEvent =
  | { method: 'stream.turn_started'; params: TurnStartedParams }
  | { method: 'stream.turn_completed'; params: TurnCompletedParams }
  | { method: 'stream.session_started'; params: SessionStartedParams }
  | { method: 'stream.text'; params: TextDeltaParams }
  | { method: 'stream.tool_use'; params: ToolUseParams }
  | { method: 'stream.tool_completed'; params: ToolCompletedParams }
  | { method: 'stream.plan_created'; params: PlanCreatedParams }
  | { method: 'stream.plan_step_started'; params: PlanStepStartedParams }
  | { method: 'stream.plan_step_completed'; params: PlanStepCompletedParams }
  | { method: 'stream.plan_step_fallback'; params: PlanStepFallbackParams }
  | { method: 'stream.plan_replanned'; params: PlanReplannedParams }
  | { method: 'stream.plan_completed'; params: PlanCompletedParams }
  | { method: 'stream.plan_escalated'; params: PlanEscalatedParams }
  | { method: 'stream.subagent.start'; params: SubAgentStartParams }
  | { method: 'stream.subagent.end'; params: SubAgentEndParams }
  | { method: 'stream.compaction'; params: CompactionParams }
  | { method: 'stream.usage'; params: UsageParams }
  | { method: 'stream.heartbeat'; params: HeartbeatParams }
  | { method: 'permission.request'; params: PermissionRequestParams }
  | { method: 'permission.response'; params: PermissionResponseParams }

// NOTE: ExecutionNode is NOT here. That is #435 Reducer's output type.
// This schema is ONLY about what comes over the wire from daemon.

2. Params type definitions for each event

Document the exact JSON shape daemon sends. Derived from:

  • StreamEventHandler.java callbacks
  • StreamingNotificationHandler.java serialization
  • PlanEventListener.java callbacks

3. New events specification (for daemon to implement in #431)

// stream.turn_started
interface TurnStartedParams {
  sessionId: string;
  requestId: string;
  turnNumber: number;
  timestamp: string; // ISO-8601
}

// stream.turn_completed
interface TurnCompletedParams {
  sessionId: string;
  requestId: string;
  turnNumber: number;
  durationMs: number;
  toolCount: number;
  timestamp: string;
}

// stream.session_started
interface SessionStartedParams {
  sessionId: string;
  model: string;
  timestamp: string;
}

// stream.plan_step_fallback
interface PlanStepFallbackParams {
  sessionId: string;
  planId: string;
  stepId: string;
  stepIndex: number;
  fallbackApproach: string;
  attempt: number;
}

4. Snapshot interoperability rules

  • Browser loads snapshot (current state)
  • Attaches WebSocket (live events)
  • Ignores already-applied events via lastEventId monotonic ordering
  • Continues rendering without rebuild

5. Forward-compatibility hooks

Reserve extension points for:

  • Swarm events (Tier 3): stream.swarm_*, stream.worker_*
  • New tool types
  • Custom event namespaces

Success Criteria

  • TypeScript types cover all 20+ daemon events (flat wire format)
  • New events (turn_started, turn_completed, session_started) fully specified
  • Schema contains ZERO tree semantics (no parentId, no nodeType, no children)
  • A browser client can pass events directly to Reducer without transformation
  • Permission request/response can be correlated by requestId
  • Snapshot + live stream semantics documented (eventId ordering)
  • Existing CLI protocol remains unchanged
  • Contract documented in-repo (aceclaw-dashboard/src/types/events.ts)

Out of Scope

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    frontendFrontend visualization and dashboardp1Priority 1 roadmap itemquestionFurther information is requestedtier1-visualizationTier 1: ReAct real-time execution tree

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions