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
Open
Conversation
…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>
Contributor
|
@iroiro147 is attempting to deploy a commit to the Vercel Team on Vercel. A member of the Team first needs to authorize it. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Second reducer-idempotency half of #1507. The first half (preserve
approval-respondedacrossinput.requestedreplays) shipped separately; this one covers the duplicate-text class.upsertRunpreviously appended a new part whenever the latest same-step run wasdone, with no deduplication. When the resume stream's cursor sits behindsession.streamIndex(see the issue's root-cause trace), the turn stream re-delivers events the client already consumed — and the reducer appended a seconddonetext part for the samestepIndex.The reducer now checks: if the incoming
text(orreasoning) 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 — thetext → tool call → more textmulti-run pattern the reducer explicitly supports is preserved.Why a prefix check (not exact equality)
message.appendedsnapshots are cumulative: each event carriesmessageSoFarfor 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-
streamIndexbug inuseEveAgentis a hook-level concern that requires touchingClientSession's#F/#i/#rinternals; 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:
message.appended("Hel") → message.appended("Hello") → message.completed("Hello")twice, asserts a singledonetext part for the step, unchanged on second pass.message.completedevents with different text for the samestepIndexstill 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/client— 90/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)