Skip to content

fix(client): skip replayed message.appended / message.completed when the step's text run is already done - #1529

Open
iroiro147 wants to merge 1 commit into
vercel:mainfrom
iroiro147:fix/message-appended-idempotent-1507
Open

fix(client): skip replayed message.appended / message.completed when the step's text run is already done#1529
iroiro147 wants to merge 1 commit into
vercel:mainfrom
iroiro147:fix/message-appended-idempotent-1507

Conversation

@iroiro147

Copy link
Copy Markdown
Contributor

What

Second reducer-idempotency half of #1507. The first half (preserve approval-responded across input.requested replays) shipped separately; this one covers the duplicate-text class.

upsertRun previously appended a new part whenever the latest same-step run was done, with no deduplication. When the resume stream's cursor sits behind session.streamIndex (see the issue's root-cause trace), the turn stream re-delivers events the client already consumed — and the reducer appended a second done text part for the same stepIndex.

The reducer now checks: if the incoming text (or reasoning) snapshot is a prefix of the last done run's recorded text (including exact equality), the upsert is declined as a replay. New turns producing different text for the same step continue to append a new part — the text → tool call → more text multi-run pattern the reducer explicitly supports is preserved.

Why a prefix check (not exact equality)

message.appended snapshots are cumulative: each event carries messageSoFar for the full stream-prefix. A replayed mid-stream event therefore carries a strict prefix of the run's terminal text, never a suffix or disjoint string. The rule "skip when done-text startsWith incoming-text" matches exactly the replay shape, and nothing else.

What's not fixed here

The resume-from-stale-streamIndex bug in useEveAgent is a hook-level concern that requires touching ClientSession's #F/#i/#r internals; see the issue for the author's own breakdown. This patch makes the reducer itself idempotent so duplicate deliveries from any source (resume, reconnect, multi-session) are safe — a strict superset of what stale-streamIndex produces.

Tests

Adds two reducer tests:

  1. Replay dedup — drives message.appended("Hel") → message.appended("Hello") → message.completed("Hello") twice, asserts a single done text part for the step, unchanged on second pass.
  2. Multi-run preserved — two message.completed events with different text for the same stepIndex still produce two parts (["First response.", "Second response."]), confirming the prefix guard doesn't swallow legitimate new runs.

pnpm exec vitest run --config vitest.unit.config.ts src/client90/90 tests green (15 reducer + 75 adjacent).
pnpm exec tsc -p tsconfig.json --noEmit — clean.

Linked issue

Refs #1507 (second reducer branch; the hook-level resume-from-stale-streamIndex concern is out of scope here)

…the step's text run is already done

`upsertRun` previously appended a fresh part whenever the latest same-step
run was `done`, so a stale resume-stream cursor replaying past events could
duplicate a completed text part for the same `stepIndex`. (vercel#1507 describes
this as the second half of the broader replay-idempotency class.)

The reducer now checks: if the incoming snapshot's text is a prefix of the
last done run's recorded text (including exact equality), the upsert is
declined as a replay. New turns producing different text for the same step
continue to append a new part — the `text → tool call → more text`
multi-run pattern the reducer explicitly supports is unchanged.

Complements the prior partial fix for vercel#1507 (input.requested preservation).
Together they cover both non-idempotent branches the issue identifies.

Regression coverage in `message-reducer.test.ts`:
- drives `message.appended("Hel") → message.appended("Hello") →
  message.completed("Hello")` twice, asserts a single done part survives
- control test: different text on the same step still appends a new run

Verified:
- `pnpm exec vitest run --config vitest.unit.config.ts src/client` — 90/90 tests green
- `pnpm exec tsc -p tsconfig.json --noEmit` — clean

Refs vercel#1507 (partial — covers the second reducer branch; the resume-from-
stale-streamIndex fix is a useEveAgent hook concern and out of scope here)

Signed-off-by: Sarthak Singh <sarthak.singh@juspay.in>
@vercel

vercel Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

@iroiro147 is attempting to deploy a commit to the Vercel Team on Vercel.

A member of the Team first needs to authorize it.

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