Skip to content

fix: prevent request_id desync from background events and duplicate Stopped - #40

Merged
lukemarsden merged 1 commit into
mainfrom
fix/request-id-desync-background-events
Apr 14, 2026
Merged

fix: prevent request_id desync from background events and duplicate Stopped#40
lukemarsden merged 1 commit into
mainfrom
fix/request-id-desync-background-events

Conversation

@lukemarsden

Copy link
Copy Markdown

Summary

  • Fix systematic n-1 response shift where each message gets the response for the previous message
  • EntryUpdated now uses turn-scoped request_id with prev_turn fallback for background events from Claude Code
  • NewEntry only updates turn_request_id at turn boundaries, rotating old value into prev_turn_request_id
  • Guard normal-completion Stopped emission with stopped_emitted check to prevent duplicate message_completed (Critical Fix build(deps): bump the npm_and_yarn group across 1 directory with 2 updates #9 in portingguide.md)

Test plan

  • E2E tests pass for both zed-agent and claude (all 10 phases including Phase 8 mid-stream interrupt and Phase 9 rapid 3-turn cancel)
  • cargo check -p external_websocket_sync and cargo check -p acp_thread pass
  • Deploy to sandbox and verify n-1 desync no longer occurs with follow-up messages

🤖 Generated with Claude Code

…topped

Three fixes for the systematic n-1 response shift where each message
gets the response for the previous message:

1. EntryUpdated uses turn-scoped request_id with prev_turn fallback:
   Claude Code delivers background events (tool completions, text
   flushes) asynchronously via session_notification after a turn ends.
   EntryUpdated now checks whether the entry belongs to the current
   turn (>= turn_start) or a previous one, and tags it with the
   correct turn's request_id. Previously it read the global
   THREAD_REQUEST_MAP which could already point to the next turn.

2. NewEntry updates turn_request_id only at turn boundaries:
   turn_request_id is now only updated from the global map when
   current == last_completed (i.e. a new turn is starting). The old
   value is rotated into prev_turn_request_id for use by late
   EntryUpdated events from the previous turn.

3. Guard normal-completion Stopped against duplicate emission:
   When cancel() races with natural turn completion, both paths could
   emit Stopped. The duplicate triggers the stale-detection fallback
   which reads the global map (now pointing to the next turn's
   request_id), sending a premature message_completed that shifts all
   subsequent responses by one. Added stopped_emitted_for_task check
   to the normal completion path, matching the existing guard on the
   cancelled path. Documented as Critical Fix #9 in portingguide.md.

E2E tests pass for both zed-agent and claude (all 10 phases).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
lukemarsden added a commit to helixml/helix that referenced this pull request Apr 14, 2026
Bump ZED_COMMIT to pick up helixml/zed#40 which fixes the
systematic n-1 response shift caused by:
- Background events from Claude Code tagged with wrong request_id
- Duplicate Stopped events from cancel/completion race

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@lukemarsden
lukemarsden merged commit 1f22a85 into main Apr 14, 2026
19 checks passed
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