Skip to content

feat(console): reactive traces + session-events live streams - #224

Merged
ytallo merged 4 commits into
mainfrom
feat/traces-live-stream
Jun 4, 2026
Merged

feat(console): reactive traces + session-events live streams#224
ytallo merged 4 commits into
mainfrom
feat/traces-live-stream

Conversation

@ytallo

@ytallo ytallo commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

What

Makes the console reactive by subscribing directly to engine streams/triggers, removing the harness fanout relays:

  • Traces view live-refreshes off the engine trace trigger instead of 3 s polling.
  • Chat session events stream directly from agent::events (a session-scoped stream trigger) instead of the harness ui::session::event fanout.

Details

  • Traces live-refreshtraces-live.ts registers an iii::console::traces_changed handler bound to a trace trigger; useTraceData/useTraceGroups drop refetchInterval; the open trace's detail tree reloads silently on each tick (no flicker, no reselect); auto-pause-on-select removed so the list stays live.
  • Session events live streamsession-events-live.ts binds a group_id-scoped agent::events stream trigger per session, delivered straight to the browser; the harness agent-events fanout pump (spawnAgentEventsPump, ui::subscribe → per-browser push) is removed.
  • iii:: prefix — both handlers are iii::-prefixed, so their delivery spans are tagged iii.function.kind=internal (hidden by the default include_internal:false query) and skipped by the engine trigger loop-break — no traces_changed / session_event spam in the trace list.
  • SearchdedupeToTraceRoots collapses search_all_spans results to one row per trace, so an operation search (e.g. harness::trigger) returns the trace, not its whole span dump.
  • Engine-routing hideisEngineRoutingSpan now requires a function_id attribute, so a worker's own call <fn> span (no function_id) is no longer swept up with the engine's call <fn> routing spans.

Depends on

Test plan

  • pnpm test (console) — 519 pass, incl. traces-live, session-events-live, dedupeToTraceRoots, engine-routing
  • pnpm typecheck — clean (excl. pre-existing storybook resolution errors)
  • Manual: chat streams + traces live-refresh + detail stream against an engine with #1753

Summary by CodeRabbit

  • New Features

    • Added live auto-refresh for traces that automatically updates when the engine detects span activity.
    • Replaced polling-based updates with real-time signal-driven refresh mechanism.
  • Bug Fixes

    • Fixed trace list display to show one row per trace, even when search results include multiple spans.
    • Improved detection of engine routing spans to require proper identification markers.
  • Chores

    • Removed harness-based agent event fan-out; browsers now subscribe directly to engine events.
    • Updated system architecture documentation to reflect new event streaming approach.

ytallo added 3 commits June 3, 2026 05:25
Subscribe each chat session directly to its `agent::events` stream via a
scoped engine stream trigger (group_id = session_id), consumed in the
console (session-events-live.ts), replacing the harness fanout hop
(`ui::subscribe` → per-browser `ui::session::event` push). Drops the
harness agent-events pump. The handler is `iii::`-prefixed so its delivery
spans are engine-internal — hidden from the traces view and skipped by the
trigger loop-break, matching `traces-live.ts`.

Also collapse trace search results to one row per trace
(`dedupeToTraceRoots`): an operation search uses `search_all_spans`, which
returns every span of a matching trace, and the flat list is one-row-per-
trace.
@vercel

vercel Bot commented Jun 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
workers Ready Ready Preview, Comment Jun 4, 2026 6:15pm

Request Review

@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 237c3532-ac74-4946-a2d7-9c533328ba70

📥 Commits

Reviewing files that changed from the base of the PR and between 99899e6 and 5365182.

📒 Files selected for processing (1)
  • harness/src/harness/fanout/index.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • harness/src/harness/fanout/index.ts

📝 Walkthrough

Walkthrough

This PR transitions the console from harness-fanned agent events to direct per-session agent::events subscriptions, implements live-refresh for the Traces view with debounce coalescing, strengthens engine routing span classification with function_id attribute checks, and removes the harness-side fanout pump infrastructure.

Changes

Frontend Agent Events Subscription and Traces Live Refresh

Layer / File(s) Summary
Session Events Direct Subscription
console/web/src/lib/backend/session-events-live.ts, console/web/src/lib/backend/session-events-live.test.ts, console/web/src/lib/backend/real.ts, console/web/src/lib/backend/types.ts, console/web/src/lib/iii-client.ts, console/web/src/types/iii-agent-event.ts
New extractSessionEvent and startSessionEventsSubscription functions implement direct subscription to per-session agent::events stream via group_id-scoped triggers, replacing UI-harness fanout. realStream integrates the subscription and cleanup. Tests verify frame parsing, session filtering, handler/trigger registration, and cleanup behavior. Removed SessionEventEnvelope wrapper type.
Traces Live Refresh Implementation
console/web/src/lib/traces-live.ts, console/web/src/lib/traces-live.test.ts, console/web/src/pages/Traces/hooks/useTraceGroups.ts, console/web/src/pages/Traces/hooks/useTraceData.ts, console/web/src/pages/Traces/index.tsx, console/web/src/pages/Traces/components/TraceGroupsView.tsx
New traces-live module implements engine-triggered trace refresh with trailing-edge debounce, visibility-aware cache invalidation, and reconnect/tab-visibility resync. Hooks now disable polling (refetchInterval: false) and rely on useTracesLiveRefresh for invalidation. Traces page supports silent reloads for selected trace waterfall via loadTraceSpans({ silent }). Removed isPaused option from hooks and prop from TraceGroupsView.
Trace Root Deduplication for Search Results
console/web/src/pages/Traces/lib/traceListItem.ts, console/web/src/pages/Traces/lib/traceListItem.test.ts
New dedupeToTraceRoots helper collapses search-all-spans responses to one row per trace by selecting root spans (no parent_span_id) or earliest spans. Integrated into useTraceData to prevent multi-row duplicates from search results.
Engine Routing Span Classification Strengthened
console/web/src/pages/Traces/lib/spanLabel.ts, console/web/src/pages/Traces/lib/spanLabel.test.ts, console/web/src/pages/Traces/lib/spanTree.test.ts
isEngineRoutingSpan now requires function_id attribute in addition to name prefix check to prevent misclassifying harness worker call <fn> spans. Test helper makeSpan auto-computes function_id for engine-routing spans. Assertions updated to verify both name and attribute markers.

Harness Agent Events Pump Removal

Layer / File(s) Summary
Fanout Pump Code Removal and Documentation Updates
harness/src/harness/fanout/index.ts, harness/src/harness/main.ts, harness/src/index.ts, harness/docs/architecture.md, harness/docs/workers/harness.md
Removed spawnAgentEventsPump initialization from spawnPumps. Updated harness worker descriptions and architecture documentation to remove references to agent::events fanout to subscribed browsers, reflecting that browsers now subscribe directly to the engine stream with group_id-scoped triggers.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • iii-hq/workers#210: Introduces per-browser trigger handler registration infrastructure that the new startSessionEventsSubscription directly depends on for wiring session-scoped agent::events stream triggers.
  • iii-hq/workers#156: Updates harness-node emission of agent::events items to use standardized event payload shape keyed to session group_id, which the new extractSessionEvent parser directly consumes.
  • iii-hq/workers#174: Introduces unified AgentEvent translation bridge and updates typings to support a single message-complete flow that bridges harness event translation to console frontend streaming.

Suggested reviewers

  • andersonleal
  • sergiofilhowz

Poem

A rabbit leaps through subscription streams, 🐰
Trading fanout for direct, focused beams,
Traces now refresh when engines tick,
No polling pause—the live signal does the trick,
Harness steps back; browsers take the lead! 🎯

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 55.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: adding reactive traces live-refresh via engine triggers and direct session-events streaming, which are the primary focus of this changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/traces-live-stream

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
console/web/src/lib/backend/session-events-live.test.ts (1)

65-104: ⚡ Quick win

Add a regression test for trigger-registration failure cleanup.

Please add a case where on() succeeds but registerTrigger() throws, and assert the handler unregister (offHandler) is called. This locks in rollback behavior and prevents future listener leaks.

Also applies to: 146-154

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@console/web/src/lib/backend/session-events-live.test.ts` around lines 65 -
104, Add a test to cover the regression where on() registers a handler but
registerTrigger() throws: modify the fakeClient test helper or a new test case
to have registerTrigger reject/throw (e.g., make registerTrigger = vi.fn(() => {
throw new Error(...) }) ), call the code path that invokes client.on and
client.registerTrigger, then assert offHandler (the function returned by on) is
invoked and triggerUnregister is not leaked; reference the fakeClient helper,
its on/registerTrigger/offHandler/triggerUnregister symbols and the code path
that calls client.registerTrigger to ensure cleanup is performed when
registration fails (also add the same test around the other location mentioned).
console/web/src/lib/traces-live.test.ts (1)

93-152: 💤 Low value

Consider adding a test for the hidden-tab check in makeTracesChangedHandler.

The handler skips invalidation when document.visibilityState === 'hidden', but this branch isn't directly tested. While startTracesSubscription tests cover visibility indirectly, a direct unit test would improve coverage.

it('does nothing while the tab is hidden', () => {
  const originalDoc = globalThis.document
  try {
    Object.defineProperty(globalThis, 'document', {
      value: { visibilityState: 'hidden' },
      configurable: true,
    })
    const { client, invalidateQueries } = fakeQueryClient()
    const handler = makeTracesChangedHandler(client, { current: false })
    handler()
    expect(invalidateQueries).not.toHaveBeenCalled()
  } finally {
    Object.defineProperty(globalThis, 'document', {
      value: originalDoc,
      configurable: true,
    })
  }
})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@console/web/src/lib/traces-live.test.ts` around lines 93 - 152, Add a unit
test for makeTracesChangedHandler to cover the branch where
document.visibilityState === 'hidden': create a test that temporarily stubs
globalThis.document to an object with visibilityState: 'hidden', call
makeTracesChangedHandler(client, { current: false }) using fakeQueryClient(),
invoke the handler, assert invalidateQueries is not called (and onExtra isn't
invoked if provided), and finally restore the original document in a finally
block to avoid test pollution; reference makeTracesChangedHandler and
fakeQueryClient and assert against the fake invalidateQueries mock to locate
where to add the test.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@console/web/src/lib/backend/session-events-live.ts`:
- Around line 78-90: The handler registered via client.on(SESSION_EVENT_FN, ...)
can leak if client.registerTrigger(...) throws; wrap the call to
client.registerTrigger(...) in a try/catch and, on any error, call the
unregister function returned by client.on (off) to remove the listener before
rethrowing the error; reference the existing symbols SESSION_EVENT_FN,
client.on, off, functionId, client.registerTrigger, extractSessionEvent and
onEvent so you locate the registration block and ensure off() is invoked in the
catch path to roll back the partial subscription.

---

Nitpick comments:
In `@console/web/src/lib/backend/session-events-live.test.ts`:
- Around line 65-104: Add a test to cover the regression where on() registers a
handler but registerTrigger() throws: modify the fakeClient test helper or a new
test case to have registerTrigger reject/throw (e.g., make registerTrigger =
vi.fn(() => { throw new Error(...) }) ), call the code path that invokes
client.on and client.registerTrigger, then assert offHandler (the function
returned by on) is invoked and triggerUnregister is not leaked; reference the
fakeClient helper, its on/registerTrigger/offHandler/triggerUnregister symbols
and the code path that calls client.registerTrigger to ensure cleanup is
performed when registration fails (also add the same test around the other
location mentioned).

In `@console/web/src/lib/traces-live.test.ts`:
- Around line 93-152: Add a unit test for makeTracesChangedHandler to cover the
branch where document.visibilityState === 'hidden': create a test that
temporarily stubs globalThis.document to an object with visibilityState:
'hidden', call makeTracesChangedHandler(client, { current: false }) using
fakeQueryClient(), invoke the handler, assert invalidateQueries is not called
(and onExtra isn't invoked if provided), and finally restore the original
document in a finally block to avoid test pollution; reference
makeTracesChangedHandler and fakeQueryClient and assert against the fake
invalidateQueries mock to locate where to add the test.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ca17ce6b-3353-436f-86a3-0d6c7d747ffd

📥 Commits

Reviewing files that changed from the base of the PR and between 3440a04 and 99899e6.

📒 Files selected for processing (24)
  • console/web/src/lib/backend/real.ts
  • console/web/src/lib/backend/session-events-live.test.ts
  • console/web/src/lib/backend/session-events-live.ts
  • console/web/src/lib/backend/types.ts
  • console/web/src/lib/iii-client.ts
  • console/web/src/lib/traces-live.test.ts
  • console/web/src/lib/traces-live.ts
  • console/web/src/pages/Traces/components/TraceGroupsView.tsx
  • console/web/src/pages/Traces/hooks/useTraceData.ts
  • console/web/src/pages/Traces/hooks/useTraceGroups.ts
  • console/web/src/pages/Traces/index.tsx
  • console/web/src/pages/Traces/lib/spanLabel.test.ts
  • console/web/src/pages/Traces/lib/spanLabel.ts
  • console/web/src/pages/Traces/lib/spanTree.test.ts
  • console/web/src/pages/Traces/lib/traceListItem.test.ts
  • console/web/src/pages/Traces/lib/traceListItem.ts
  • console/web/src/types/iii-agent-event.ts
  • harness/README.md
  • harness/docs/architecture.md
  • harness/docs/workers/harness.md
  • harness/src/harness/fanout/agent-events.ts
  • harness/src/harness/fanout/index.ts
  • harness/src/harness/main.ts
  • harness/src/index.ts
💤 Files with no reviewable changes (4)
  • harness/src/harness/fanout/agent-events.ts
  • harness/src/harness/fanout/index.ts
  • console/web/src/pages/Traces/components/TraceGroupsView.tsx
  • console/web/src/types/iii-agent-event.ts

Comment on lines +78 to +90
const off = client.on(SESSION_EVENT_FN, (frame: unknown) => {
const event = extractSessionEvent(frame, sessionId)
if (event) onEvent(event)
})

// `on()` registers under `<fn>::<browserId>`; the trigger must target that id.
const functionId = `${SESSION_EVENT_FN}::${client.browserId}`
const offTrigger = client.registerTrigger({
type: 'stream',
function_id: functionId,
config: { stream_name: EVENTS_STREAM, group_id: sessionId },
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle partial subscription failure rollback.

If client.registerTrigger(...) throws after client.on(...) succeeds (Line 78 → Line 85), the handler remains registered. This leaks listeners and can cause duplicate delivery behavior on later retries. Unregister off in a catch before rethrowing.

Suggested fix
 export function startSessionEventsSubscription(
   client: Pick<IiiClient, 'browserId' | 'on' | 'registerTrigger'>,
   sessionId: string,
   onEvent: (event: AgentEvent) => void,
 ): () => void {
   const off = client.on(SESSION_EVENT_FN, (frame: unknown) => {
     const event = extractSessionEvent(frame, sessionId)
     if (event) onEvent(event)
   })

   // `on()` registers under `<fn>::<browserId>`; the trigger must target that id.
   const functionId = `${SESSION_EVENT_FN}::${client.browserId}`
-  const offTrigger = client.registerTrigger({
-    type: 'stream',
-    function_id: functionId,
-    config: { stream_name: EVENTS_STREAM, group_id: sessionId },
-  })
+  let offTrigger: (() => void) | null = null
+  try {
+    offTrigger = client.registerTrigger({
+      type: 'stream',
+      function_id: functionId,
+      config: { stream_name: EVENTS_STREAM, group_id: sessionId },
+    })
+  } catch (err) {
+    off()
+    throw err
+  }

   return () => {
     off()
     try {
-      offTrigger()
+      offTrigger?.()
     } catch {
       // SDK already disposed; nothing to do.
     }
   }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@console/web/src/lib/backend/session-events-live.ts` around lines 78 - 90, The
handler registered via client.on(SESSION_EVENT_FN, ...) can leak if
client.registerTrigger(...) throws; wrap the call to client.registerTrigger(...)
in a try/catch and, on any error, call the unregister function returned by
client.on (off) to remove the listener before rethrowing the error; reference
the existing symbols SESSION_EVENT_FN, client.on, off, functionId,
client.registerTrigger, extractSessionEvent and onEvent so you locate the
registration block and ensure off() is invoked in the catch path to roll back
the partial subscription.

Resolve conflicts:
- harness/README.md: take main's rewritten README (our one-line edit to the
  old worker table is obsoleted by the rewrite; harness 'UI fanout' stays
  accurate after dropping the agent-events pump).
- harness/src/harness/fanout/index.ts: combine both sides — keep this branch's
  removal of the agent-events pump and add main's new models-changed pump.
@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

skill-check — worker

0 verified, 14 skipped (no docs/).

Layer Result
structure
vale
ai
render

Four for four. Nicely done.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants