openclaw/AUDIT_REPORT.md
Grace (Clawdbot) e239ff27f6 feat(exec-events): add top-level runId to payload
- Added runId?: string to ExecEventBase type in exec-events.ts
- Added runId to all three emitExecEvent calls (exec.started, exec.output, exec.completed)
- Fixes Crabwalk mismatch where it expects exec.runId but Gateway sent context.runId
- runId is sourced from session.execEvents.context?.runId for backward compatibility

Resolves: Crabwalk exec nodes not displaying due to schema mismatch
2026-01-28 12:07:24 +01:00

7.3 KiB

Exec Events E2E Audit Report

Date: 2026-01-27 Auditor: Grace (via Codex analysis)

Executive Summary

The exec events flow is fundamentally working (Gateway emits → WebSocket broadcasts → Crabwalk receives), but there's a critical runId schema mismatch that prevents proper session linking.

Critical Finding

Gateway sends runId nested in context object (optional); Crabwalk expects runId at top-level (required).


Part 1: Gateway Event Emission Working

Findings

Emission conditions (src/agents/bash-tools.exec.ts):

  • Line 392: execEventsConfig.emitEvents === true
  • Line 395: execEventsEnabled = execEventsConfig.emitEvents === true && execEventsMatch.matched
  • Both conditions must be true for events to emit

Emission call sites:

  • Line 577: emitExecEvent("exec.output", {...})
  • Line 595: emitExecEvent("exec.started", {...})
  • Line 618: emitExecEvent("exec.completed", {...})

Payload structure (src/infra/exec-events.ts):

type ExecEventBase = {
  event: ExecEventName;  // "exec.started" | "exec.output" | "exec.completed"
  ts: number;
  seq: number;
  sessionId: string;
  pid: number;
  command?: string;
  context?: ExecEventContext;  // 👈 runId is NESTED here
};

type ExecEventContext = {
  runId?: string;        // 👈 OPTIONAL
  toolCallId?: string;
  sessionKey?: string;
};

Issues Found

  1. Line 7 (exec-events.ts): runId?: string — runId is OPTIONAL in Gateway types
  2. Line 20: context?: ExecEventContext — entire context is OPTIONAL

Part 2: Gateway Broadcast Working

Findings

Subscription (src/gateway/server.impl.ts):

  • Line 20: import { onExecEvent } from "../infra/exec-events.js"
  • Line 406-408:
const execUnsub = onExecEvent((evt) => {
  broadcast(evt.event, evt, { dropIfSlow: true });
});

GATEWAY_EVENTS list (src/gateway/server-methods-list.ts):

  • Line 111: "exec.started"
  • Line 112: "exec.output"
  • Line 113: "exec.completed"

Status

Exec events ARE properly registered and broadcast.


Part 3: Crabwalk Parser 🔴 MISMATCH

Findings

Parser (/home/clawdbot/apps/crabwalk/src/integrations/clawdbot/parser.ts):

  • Line 200-201:
const exec = frame.payload as ExecStartedEvent
const execId = `exec-${exec.runId}-${exec.pid}`  // 👈 Uses top-level runId
  • Line 211: runId: exec.runId — expects runId at top level
  • Line 260: const execId = \exec-${exec.runId}-${exec.pid}`` — same pattern

Protocol types (/home/clawdbot/apps/crabwalk/src/integrations/clawdbot/protocol.ts):

  • Line 88-92:
export interface ExecStartedEvent {
  sessionId: string;
  pid: number;
  runId: string;    // 👈 REQUIRED string, NOT optional
  ...
}
  • Line 92: runId: string — REQUIRED, not optional

Critical Mismatch

Property Gateway Crabwalk
runId location context.runId (nested) exec.runId (top-level)
runId required No (runId?: string) Yes (runId: string)
context Optional object Not read at all

What happens when runId is undefined:

  • execId becomes "exec-undefined-<pid>" — breaks uniqueness
  • Session linking fails (see Part 5)

Part 4: Crabwalk UI Component Distinct

Findings

ExecNode.tsx (/home/clawdbot/apps/crabwalk/src/components/monitor/ExecNode.tsx):

  • Line 1-10: Distinct component using memo and Handle from react-flow
  • Line 8: data: MonitorExecProcess — uses proper exec type
  • Uses Terminal icon (lucide-react)
  • Shows command, status (running/completed/failed), duration

NOT reusing AgentNode or ChatNode — this is a dedicated exec component.


Part 5: ActionGraph Integration 🟡 Works IF sessionKey exists

Findings

ActionGraph.tsx (/home/clawdbot/apps/crabwalk/src/components/monitor/ActionGraph.tsx):

  • Line 18: import { ExecNode } from './ExecNode'
  • Line 55: exec: ExecNode as any — registered as node type
  • Line 138-143: Exec nodes added to graph
  • Line 273-282: Session→Exec edges created based on exec.sessionKey

Edge creation logic:

for (const exec of visibleExecs) {
  const key = exec.sessionKey  // 👈 REQUIRES sessionKey
  ...
  edges.push({
    id: `e-session-exec-${exec.id}`,
    source: `session-${key}`,
    target: `exec-${exec.id}`,
    ...
  })
}

Session key resolution (/home/clawdbot/apps/crabwalk/src/integrations/clawdbot/collections.ts):

  • Line 12: const runSessionMap = new Map<string, string>() — maps runId → sessionKey
  • Line 141-147: addAction() learns mapping from chat events
  • Line 233-234: addExecEvent() calls resolveSessionKey(event)

The problem: Without runId at top level, resolveSessionKey can't look up the sessionKey, so execs appear as orphan nodes without edges to sessions.


Option A: Fix Gateway (Preferred)

Bead 1: Add top-level runId to exec event payload

  1. Write test first:
// src/agents/bash-tools.exec.exec-events.test.ts
it("includes runId at top level in exec events", async () => {
  const events: ExecEventPayload[] = [];
  onExecEvent((e) => events.push(e));
  
  await runWithExecEventContext({ runId: "test-run-123" }, async () => {
    await runExecProcess({ command: "codex --help", ... });
  });
  
  const started = events.find(e => e.event === "exec.started");
  expect(started?.runId).toBe("test-run-123");  // 👈 Top level
});
  1. Update types (src/infra/exec-events.ts):
type ExecEventBase = {
  event: ExecEventName;
  ts: number;
  seq: number;
  sessionId: string;
  pid: number;
  command?: string;
  runId?: string;           // 👈 ADD top-level
  context?: ExecEventContext;  // Keep for backward compat
};
  1. Update emission (src/agents/bash-tools.exec.ts):
emitExecEvent("exec.started", {
  sessionId,
  pid,
  command,
  runId: session.execEvents.context?.runId,  // 👈 ADD this
  context: session.execEvents.context,
  ...
});

Option B: Fix Crabwalk (Alternative)

Read runId from context.runId if top-level is missing:

// parser.ts
const runId = exec.runId ?? frame.payload?.context?.runId;
const execId = `exec-${runId ?? 'unknown'}-${exec.pid}`;

Recommendation

Go with Option A — it's cleaner to have consistent payload structure at the source.


Next Steps

  1. Create CONTINUITY.md (done)
  2. Write failing test for top-level runId
  3. Update Gateway exec-events types
  4. Update emission call sites
  5. Run tests, verify passing
  6. Commit: feat(exec-events): add top-level runId to payload
  7. E2E test with Crabwalk

Line Number Reference

File Line Issue
src/infra/exec-events.ts 7 runId?: string optional
src/infra/exec-events.ts 20 context?: ExecEventContext optional
src/agents/bash-tools.exec.ts 595 emitExecEvent("exec.started",...) — missing top-level runId
/home/clawdbot/apps/crabwalk/src/integrations/clawdbot/protocol.ts 92 runId: string required
/home/clawdbot/apps/crabwalk/src/integrations/clawdbot/parser.ts 201 exec.runId accessed at top level
/home/clawdbot/apps/crabwalk/src/integrations/clawdbot/collections.ts 12 runSessionMap needs runId for lookup