Skip to content

fix(client): preserve approval-responded part across input.requested replays - #1527

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

fix(client): preserve approval-responded part across input.requested replays#1527
iroiro147 wants to merge 1 commit into
vercel:mainfrom
iroiro147:fix/input-requested-idempotent-1507

Conversation

@iroiro147

@iroiro147 iroiro147 commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

fix: preserve approval-responded tool part across replayed input.requested events

What

This is a partial fix for #1507. The issue describes two coupled reducer-idempotency bugs:

  1. HITL answers erased on replay — fixed here.
  2. Text parts duplicated on replaynot fixed here; that half needs upsertRun to detect "same-step done run with identical text" without breaking the multi-run pattern that lets a step emit text, call tools, then emit more text. Worth a follow-up PR with its own coverage.

This patch: preserve approval-responded across input.requested replays

When the event stream replays input.requested (e.g. after a resume), defaultMessageReducer previously rebuilt the tool part wholesale with state: "approval-requested", discarding the eve.inputResponse metadata that client.input.responded writes. The UI then showed an unanswered approval prompt even though the user had already responded.

The handler now partitions on the existing tool-part state:

  • approval-responded → restore, don't rebuild. The prior approval, input, toolMetadata, and toolName are carried forward verbatim; only stepIndex and toolCallId refresh from the event. We deliberately do not merge approval fields: the union member for approval-requested (approved?: never; reason?: never) is incompatible with the member for approval-responded (approved?: boolean; reason?: string), so blending them erases the recorded answer.
  • Otherwise → fresh approval-requested. Still emits the part, but merges any prior toolMetadata with the newly-derived descriptor via mergeToolMetadata — the same pattern action.result uses a few branches below.

One nuance: parts in terminal states (output-available / output-error / output-denied) also fall into the "otherwise" branch today. That mirrors upstream behavior (a re-request after a terminal result legitimately starts a new approval cycle in the reducer's coarse state machine), and matching it keeps this patch low-blast-radius. Tightening terminal-state replay handling is part of the same follow-up as the duplicate-text fix.

Why this approach

Fixing the resume-from-stale-streamIndex bug (the other half of #1507) would plug the source but wouldn't make the reducer itself idempotent to duplicate input.requested events from any cause (multi-session, reconnect, hot-reload). Making the reducer idempotent is the narrower, protocol-level fix — and it's a strict superset of what the resume flow needs.

Tests

  • Adds defaultMessageReducer › replayed input.requested does not erase a prior approval-responded part (#1507) in packages/eve/src/client/message-reducer.test.ts. It drives input.requested → client.input.responded → input.requested (replay) and asserts the part stays approval-responded with the original eve.inputResponse intact and eve.inputRequest preserved.
  • Full vitest run --config vitest.unit.config.ts in packages/eve: 5872 / 5876 passing, with 4 pre-existing bin-bootstrap.test.ts failures that gate on Node 22+ (unrelated).
  • tsc -p tsconfig.json --noEmit is clean.

Linked issue

Refs #1507 (partial — see "What" above for the duplicate-text scope split).

…replays

When the event stream replays `input.requested` after a user has already
responded (e.g. during a resume), the reducer previously rebuilt the
tool part wholesale with `state: "approval-requested"`, wiping the
stored `eve.inputResponse` metadata written by `client.input.responded`.
The UI then displayed an unanswered approval prompt even though the user
had already answered.

Partition on the existing tool-part state:

- `approval-responded` → preserve `approval` / `input` / `toolMetadata`
  / `toolName` verbatim; only refresh `stepIndex` and `toolCallId` from
  the latest event. Never collapse the approval shape into
  `approval-requested`'s narrower variant.
- Otherwise (fresh request or a replay before any response) → emit a
  fresh `approval-requested` part, merging any existing `toolMetadata`
  with the newly-derived descriptor so `eve.inputRequest` stays current.

Includes a regression test that drives the full replay sequence
(input.requested → client.input.responded → replayed input.requested)
and asserts the part stays in `approval-responded` with the original
`inputResponse` intact.

Closes vercel#1507

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.

@iroiro147 iroiro147 changed the title fix(client): preserve approval-responded part across input.requested replays (#1507) fix(client): preserve approval-responded part across input.requested replays (partial fix for #1507) Aug 2, 2026
@iroiro147 iroiro147 changed the title fix(client): preserve approval-responded part across input.requested replays (partial fix for #1507) fix(client): preserve approval-responded part across input.requested replays Aug 2, 2026
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