Skip to content

fix(agent): let elapsedMs reach the wire through the AG-UI SSE schemas - #3509

Merged
kojiwakayama merged 1 commit into
mainfrom
fix/agui-sse-elapsed-passthrough
Aug 9, 2026
Merged

kojiwakayama merged 1 commit into
mainfrom
fix/agui-sse-elapsed-passthrough

Conversation

@kojiwakayama

Copy link
Copy Markdown
Contributor

elapsedMs has been stamped since #3502 but still never reached storage. This is why.

Root cause, with a reproduction

The AG-UI SSE payload schemas in src/internal-agents/ag-ui-sse.ts are an allow-list, and formatAgUiEvent serializes schema.parse(payload) — the parse result, not the input. Anything the schema does not declare is dropped between the encoder and the wire:

formatAgUiEvent("StepStarted", { stepName: "step-1", elapsedMs: 42 })
→ 'event: StepStarted\ndata: {"stepName":"step-1"}\n\n'

That also explains the evidence that made this so confusing to chase: stepName survived because it is on the allow-list; elapsedMs never was. Both symptoms had the same single cause.

Measured on production v0.1.1222: the 14:00 tick still showed no elapsedMs on any event, on pods verified to report 0.1.1222 (10.192.9.132, 10.192.6.137, artifact 20260809132722-48ee1f3b77bb). So this was not a rollout-timing artifact.

Fix

Declare elapsedMs once through a withElapsed helper applied to all 18 event schemas, rather than widening them to passthrough. The allow-list is a deliberate wire contract — this stream is persisted verbatim into agent_run_event — so it stays closed to everything else.

Tests

Two, both verified to fail without the change:

  • elapsedMs reaches the wire, and an undeclared field still does not — pinning both halves, so a future "fix" that just makes the schemas passthrough would not satisfy it.
  • End-to-end from the real production composition root: createStreamTransformState() → mapRuntimeEventToAgUi → formatAgUiEvent.

Reverting the fix reproduces the exact production symptom:

AssertionError: elapsedMs must reach the wire, got
"event: StepStarted\ndata: {\"stepName\":\"step-1\"}\n\n"

The gap that let this through: browser-encoder and ag-ui-sse were each well covered in isolation, so a field vanishing between them was invisible to both suites. The second test exists specifically to cover that boundary.

Follow-up, agreed separately

Add an absolute emittedAt as the durable primitive and derive elapsed on read. elapsedMs is measured from encoder construction, so its meaning depends on which encoder instance produced it and when — the anchor ambiguity behind #3483, #3497 and #3502 each landing in the wrong place. An absolute timestamp has no anchor to get wrong, and additionally makes ingest lag measurable as created_at − emittedAt, which is the gap that misled the original trace analysis.

Note on the pre-push gate

Two pushes were rejected by timing-sensitive tests before this landed — string-renderer (5ms SSR deadline) and project-env/fetcher (timeout fallback). Both pass in isolation and are unrelated to SSE schemas; the machine was loaded by concurrent suites. Flagging rather than hiding it.

`elapsedMs` has been stamped since #3502 but never reached storage. The
AG-UI SSE payload schemas in `ag-ui-sse.ts` are an allow-list, and
`formatAgUiEvent` serializes `schema.parse(payload)` -- the parse result,
not the input. Any field the schema does not declare is dropped between
the encoder and the wire:

  formatAgUiEvent("StepStarted", { stepName: "step-1", elapsedMs: 42 })
  -> 'event: StepStarted\ndata: {"stepName":"step-1"}\n\n'

That also explains the confusing evidence this produced: `stepName`
survived because it is on the allow-list, `elapsedMs` never was. Measured
on production v0.1.1222, the 14:00 tick still showed no `elapsedMs` on any
event despite running on pods that report 0.1.1222.

Declare `elapsedMs` once via a `withElapsed` helper applied to all 18
event schemas, rather than widening them to passthrough. The allow-list is
a deliberate wire contract -- this stream is persisted verbatim into
`agent_run_event` -- so it stays closed to everything else.

Two tests pin both halves, and both fail without this change: the field
reaches the wire, and an undeclared field still does not. The second
drives the real production encoder root (`createStreamTransformState`)
through `formatAgUiEvent`, which is the boundary nothing covered before --
`browser-encoder` and `ag-ui-sse` were each tested in isolation, so the
field being dropped between them was invisible.

Follow-up agreed separately: add an absolute `emittedAt` as the durable
primitive and derive elapsed on read. `elapsedMs` is measured from encoder
construction, so its meaning depends on which encoder instance produced it
-- the anchor ambiguity behind #3483, #3497 and #3502 all landing in the
wrong place. An absolute timestamp has no anchor, and additionally makes
ingest lag measurable as `created_at - emittedAt`.
@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@kojiwakayama, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 4 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 74c90dd4-0c63-45e4-a17f-a692b9a56097

📥 Commits

Reviewing files that changed from the base of the PR and between c8cab33 and fb8e052.

📒 Files selected for processing (2)
  • src/internal-agents/ag-ui-sse.test.ts
  • src/internal-agents/ag-ui-sse.ts

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

@kojiwakayama
kojiwakayama added this pull request to the merge queue Aug 9, 2026
Merged via the queue into main with commit d1d03a5 Aug 9, 2026
31 checks passed
@kojiwakayama
kojiwakayama deleted the fix/agui-sse-elapsed-passthrough branch August 9, 2026 14:36
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.

1 participant