# 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): ```typescript 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: ```typescript 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: ```typescript 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: ```typescript 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-"` β€” 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:** ```typescript 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()` β€” 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. --- ## Recommended Fixes (Test-First Approach) ### Option A: Fix Gateway (Preferred) **Bead 1: Add top-level runId to exec event payload** 1. Write test first: ```typescript // 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 }); ``` 2. Update types (src/infra/exec-events.ts): ```typescript 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 }; ``` 3. Update emission (src/agents/bash-tools.exec.ts): ```typescript 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: ```typescript // 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 |