13-ai-condorwave: W6filled6 citations

Condor SSE / AG-UI Frame Pump — Emitter → Mapper → Wire → Consumer

Audiences: developer, internal

Condor SSE / AG-UI Frame Pump — Emitter → Mapper → Wire → Consumer

The pump is how agent work becomes pixels. Condor's runtime drains the Mastra / AI-SDK stream, mints typed data-* frames at the tool edge, projects them into AG-UI events, and ships them over a dedicated raw RFC 8895 SSE transport that is deliberately not the HMS @ddx/sse-contract bus. The subsystem is ddx-api/src/ai/condor/{pump,event-bus}/ (19 files) plus the four top-level agui-* files that own framing and projection.

The frame path

Rendering diagram…

Two things ride the same emitter but reach the browser by different roads: the AI-SDK UIMessage data-* part goes straight through pipeUIMessageStreamToResponse to useChat, while the AG-UI projection above feeds the inspector on /agui/live. data-intent is the canonical example — the mapper forwards it verbatim precisely so neither surface loses it (agui-orchestrator-mapper.ts:440-451).

⚠ The prefix rule — data-condor-* means two different things

This is the load-bearing contract of the whole topic. Getting it backwards produces a frame that is tsc-green, lint-green, grep-green, and renders an empty panel in the browser. The mapper handles frames through two disjoint routes, and the correct prefix depends on which route your frame takes.

Class 1 — explicitly-cased frames: data-condor-* is CORRECT

The settled tool / doc / plan frame set is hand-mapped by an explicit case block, which strips and re-maps the type deliberately:

Emitted frame (condor-tool-stream-emitter.ts:95-126)MapperProjected AG-UI name
data-condor-tool-started:298condor.tool.started (+ ToolCallStart)
data-condor-tool-progress:325condor.tool.progress
data-condor-tool-completed:331condor.tool.completed
data-condor-tool-failed:349condor.tool.failed
data-condor-tool-cardToolCard
data-condor-doc-generation-start:374condor.document.*
data-condor-plan-revised:419condor.plan-revised
data-condor-run-suspended:487HITL suspend frame

For every one of these, data-condor- is the real, live prefix. Do not "fix" them.

Class 2 — fallthrough frames: data-condor-* is a BUG

Any frame type without a case falls through to the generic projector:

ts
name: `condor.${type.slice('data-'.length)}`   // :433, :457, :549

That expression strips only data-. It does not strip condor-. So a new frame emitted as data-condor-foo projects to condor.condor-foo, and a consumer subscribing to condor.foo matches nothing and renders empty.

Live proof it already happened once: condor.condor-run is a real, surviving, mis-prefixed channel the frontend had to accommodate rather than rename (ddx-web/src/lib/condor/agui-artifacts.ts:443-445). The sub-agent lane hit the same trap on 2026-07-23 and was fixed the correct way — its frames are data-subagent-spawn / -complete / -failed, with the reasoning recorded inline at condor-tool-stream-emitter.ts:103-115, and the web lane subscribes to condor.subagent-* (ddx-web/src/lib/condor/agui-types.ts:240-242).

Class 3 — payload-named cased frames: data-condor-* is CORRECT (different reason)

A third route exists and is the one an author is most likely to copy when adding a new data-condor-* frame — so it must not be mistaken for Class 2. The capability / plan-mutation frames have an explicit case but derive their channel name from the payload, falling back to a slice that strips the FULL data-condor- prefix (not just data-):

ts
case 'data-condor-capability':
case 'data-condor-plan-mutation': {
  const eventName = str(d.event) ?? `condor.${type.slice('data-condor-'.length)}`   // :470-473

slice('data-condor-'.length) strips data-condor-, so data-condor-capabilitycondor.capability — NO doubling. This is safe BECAUSE it has an explicit case with a longer slice; it is NOT the generic Class-2 fallthrough. Copying this block's data-condor-* shape into a frame that then hits the Class-2 fallthrough (no case) is exactly how the doubling bug is introduced.

RULE. The prefix depends on the ROUTE, not the name:

  • Explicit case, re-mapped or full-prefix-sliced (Class 1 & 3)data-condor-* is correct.
  • Generic fallthrough, no case (Class 2) → must be data-<suffix> with no condor-, or it double-prefixes to condor.condor-*.

Before adding a frame: is there a case for it? If yes and it slices 'data-condor-'.length or re-maps explicitly → data-condor-*. If it will fall through the generic projector → data-<suffix>. When unsure, trace the exact .slice(...) your type hits. Verify in a browser — neither the type-checker nor a grep can catch this class.

The two-bus model — and the inert half

Condor carries two event surfaces, and they are not peers:

  1. The outward AG-UI/SSE transport (live). agui-sse.helper.ts frames raw RFC 8895 (id: / event: / data:), agui-event-store.service.ts persists a per-run monotonic seq for Last-Event-ID replay, and GET runs/:runId/agui/live serves it through a 3-tier resolver (durable store → live registry claim → span replay).
  2. The internal EDA bus (degraded, observability-only). event-bus/event-bus.bridge.ts is a module-level seam letting non-DI code (Mastra tool wrappers) publish without a NestJS dependency. As verified 2026-07-18, nothing calls setEventBusPublisherpublisher stays null for the process lifetime, so publishBusEventSafe, publishBusEventDualSafe and EventBusFacade.publish are all silent no-ops. Callers are written to tolerate this, so it is degraded rather than broken. Note the asymmetry: EventBusFacade itself is not dormant — its tryClaim() is live and backs plan dedupe in pump/phases/plan-phase.ts.

The practical consequence: if you need a frame to reach the UI, emit it through the pump/emitter path, never through the event bus. Do not read the presence of the bridge seam as evidence a publisher exists — grep for call sites of setEventBusPublisher, not mentions of it.

Pump phases

pump/ splits the drain into phase files rather than one loop: dispatch-phase.ts (turn entry), plan-phase.ts (plan emission + facade-backed dedupe), suspend-resume-phase.ts + hitl-suspend.ts + enqueue-suspend-frame.ts (the HITL gate — see Condor HITL Approval), finalize-phase.ts and emit-run-recap.ts (terminal frames), with zero-execution-guard.ts catching plans that would produce no work. pump-context.ts, run-knobs.ts and stream-readers.ts carry the shared per-run state and stream adapters.

Relationship to @ddx/sse-contract

@ddx/sse-contract (4.1.0) is the HMS global SSE bus — canonical event registry, SseEnvelope v3, SSEChannels resolvers. The Condor AG-UI inspector wire is a deliberately disjoint transport: /agui/live serves raw RFC 8895 frames and is not wrapped in SseEnvelope/SSEChannels, because each resolver tier owns its own id: cursor (integer seq on the durable path, ISO-8601 startedAt on the span-replay path) and an envelope wrap would break replay. This exemption is asserted in the helper header (agui-sse.helper.ts:10-14) and in the controller invariants.

That exemption is scoped to this one transport. Everything else in HMS — including any Condor code publishing to the platform bus — still MUST use @ddx/sse-contract: no raw agent:session-${id} literals, no _meta v2 envelopes, no new v3-gating flags. See SSE Event Engine.

  • Condor HITL Approval — suspend/approve frames ride this same pump and event-bus wire.
  • Condor Engine — the runtime, controller and 3-tier /agui/live resolver this pump feeds.
  • SSE Event Engine — the @ddx/sse-contract global bus this transport is deliberately exempt from.
  • FlowAgent AG-UI Bridge — the sibling AG-UI projection in the voice/flow estate.