From bc5de289f48ee66903ce9959dd13469aabebcbb0 Mon Sep 17 00:00:00 2001 From: AccountableHumanity Date: Sun, 3 May 2026 06:46:59 +0000 Subject: [PATCH] chore(debug): add flag-gated event tracing to bridge + session panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a tiny debug-flag module (web/lib/debug-flag.ts) and dlog tracepoints inside the SSE→bridge→panel pipeline so the next time the intermittent non-rendering bug recurs we can capture ground-truth data instead of guessing. Enable via: - URL: http://localhost:3000/?debug=events - Or: localStorage.setItem('AGENT_FLOW_DEBUG', 'events') When the flag is off, dlog is a single-branch no-op — no behavioral change to the bridge or panel. Tracepoints: - bridge.onEvent: per-event arrival with type, sessionId, drop-mode, selected-match - droppingRef early-return path - per-session buffer push - sessionsWithActivity DEDUP vs ADD branches - flushSessionEvents entry - dismissed-session re-add path - rAF eventVersion bump - SessionCanvasPanel render: logLen / newEvents / consumed Co-Authored-By: Claude Opus 4.7 (1M context) --- .../agent-visualizer/session-canvas-panel.tsx | 3 ++ web/hooks/use-vscode-bridge.ts | 13 ++++++++- web/lib/debug-flag.ts | 29 +++++++++++++++++++ 3 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 web/lib/debug-flag.ts diff --git a/web/components/agent-visualizer/session-canvas-panel.tsx b/web/components/agent-visualizer/session-canvas-panel.tsx index d2541db..15c5690 100644 --- a/web/components/agent-visualizer/session-canvas-panel.tsx +++ b/web/components/agent-visualizer/session-canvas-panel.tsx @@ -16,6 +16,7 @@ import { CanvasZoomControl } from './canvas-zoom-control' import { useSessionStatsDispatch, type SessionStats } from './session-stats-provider' import { TIMING, type SimulationEvent, type TimelineEvent } from '@/lib/agent-types' import type { EffectToggles } from '@/hooks/use-perf-settings' +import { dlog } from '@/lib/debug-flag' /** Stable empty-events sentinel — keeps the externalEvents prop reference * identical across idle renders so useAgentSimulation's animate callback @@ -80,6 +81,8 @@ function SessionCanvasPanelImpl({ ? (log.slice(consumedRef.current, sliceEnd) as SimulationEvent[]) : EMPTY_EVENTS + dlog('panel render', sessionId.slice(0, 8), 'logLen=', log.length, 'newEvents=', newEvents.length, 'consumed=', consumedRef.current) + const sim = useSessionSimulation(manager, sessionId, { externalEvents: newEvents, onExternalEventsConsumed: () => { consumedRef.current = sliceEnd }, diff --git a/web/hooks/use-vscode-bridge.ts b/web/hooks/use-vscode-bridge.ts index 7f74ff4..65a0154 100644 --- a/web/hooks/use-vscode-bridge.ts +++ b/web/hooks/use-vscode-bridge.ts @@ -3,6 +3,7 @@ import { useEffect, useMemo, useState, useCallback, useRef } from 'react' import { vscodeBridge, type ConnectionStatus, type AgentEvent, type SessionInfo } from '@/lib/vscode-bridge' import { SimulationEvent } from '@/lib/agent-types' +import { DEBUG_EVENTS, dlog } from '@/lib/debug-flag' interface BridgeHookResult { isVSCode: boolean @@ -153,10 +154,12 @@ export function useVSCodeBridge(): BridgeHookResult { // selectedSessionIdRef is updated synchronously (not via React state) so it's // always current even before React re-renders. const unsubEvent = bridge.onEvent((event: AgentEvent) => { + dlog('event', event.type, 'sid=', (event.sessionId ?? '').slice(0, 8), 'drop=', droppingRef.current, 'selected=', selectedSessionIdRef.current === event.sessionId) // Drop the relay's initial backlog. Each event resets the idle timer; // once IDLE_FLUSH_MS passes quietly, the backlog has finished arriving // and we exit drop mode for normal live handling. if (droppingRef.current) { + dlog(' dropped (warmup)') if (idleTimerRef.current) clearTimeout(idleTimerRef.current) idleTimerRef.current = setTimeout(() => exitDropRef.current(), IDLE_FLUSH_MS) return @@ -174,6 +177,7 @@ export function useVSCodeBridge(): BridgeHookResult { const buf = sessionEventsRef.current.get(event.sessionId) || [] buf.push(simEvent) sessionEventsRef.current.set(event.sessionId, buf) + dlog(' buffered, log size now', buf.length) } // Deliver to pending if session matches (ref is always current). @@ -184,7 +188,11 @@ export function useVSCodeBridge(): BridgeHookResult { pendingEventsRef.current.push(simEvent) } else if (event.sessionId && event.sessionId !== selected) { setSessionsWithActivity(prev => { - if (prev.has(event.sessionId!)) return prev + if (prev.has(event.sessionId!)) { + dlog(' sessionsWithActivity DEDUP (already had id)') + return prev + } + dlog(' sessionsWithActivity ADD') const next = new Set(prev) next.add(event.sessionId!) return next @@ -196,6 +204,7 @@ export function useVSCodeBridge(): BridgeHookResult { if (!rafPendingRef.current) { rafPendingRef.current = true rafIdRef.current = requestAnimationFrame(() => { + dlog(' rAF eventVersion bump') rafPendingRef.current = false rafIdRef.current = null setEventVersion(v => v + 1) @@ -204,6 +213,7 @@ export function useVSCodeBridge(): BridgeHookResult { // Re-add dismissed sessions when new events arrive if (event.sessionId && dismissedSessionsRef.current.has(event.sessionId)) { + dlog(' dismissed-session re-add for', event.sessionId.slice(0, 8)) const saved = dismissedSessionsRef.current.get(event.sessionId) dismissedSessionsRef.current.delete(event.sessionId) if (saved) { @@ -335,6 +345,7 @@ export function useVSCodeBridge(): BridgeHookResult { /** Flush buffered events for the selected session into pending. * Must be called from useLayoutEffect AFTER simulation state is saved/swapped. */ const flushSessionEvents = useCallback((sessionId: string, fromIndex = 0) => { + dlog('flushSessionEvents', sessionId.slice(0, 8), 'buffered=', (sessionEventsRef.current.get(sessionId)?.length ?? 0), 'fromIndex=', fromIndex) sessionSwitchPendingRef.current = false const buffered = sessionEventsRef.current.get(sessionId) || [] pendingEventsRef.current.length = 0 diff --git a/web/lib/debug-flag.ts b/web/lib/debug-flag.ts new file mode 100644 index 0000000..c1b94e2 --- /dev/null +++ b/web/lib/debug-flag.ts @@ -0,0 +1,29 @@ +/** + * Diagnostic event tracing flag for the bridge → canvas event pipeline. + * + * Read once at module load. Enable by either: + * 1. URL: ?debug=events (or any value containing the substring "events") + * 2. localStorage: localStorage.setItem('AGENT_FLOW_DEBUG', 'events') + * + * Falsy/missing means silent. The flag is cached, so toggling at runtime + * requires a page refresh. + * + * Examples: + * http://localhost:3000/?debug=events + * localStorage.setItem('AGENT_FLOW_DEBUG', 'events') // then refresh + */ +export const DEBUG_EVENTS = (() => { + if (typeof window === 'undefined') return false + try { + const url = new URLSearchParams(window.location.search).get('debug') ?? '' + if (url.includes('events')) return true + const ls = window.localStorage?.getItem('AGENT_FLOW_DEBUG') ?? '' + if (ls.includes('events')) return true + } catch { /* ignore */ } + return false +})() + +/** Cheap no-op when flag is off. Prefix all output with `[af]`. */ +export function dlog(...args: unknown[]): void { + if (DEBUG_EVENTS) console.log('[af]', ...args) +}