Fix Claude agent race condition in forwardPromptEvents

The race condition occurred when overlapping stream() calls (e.g., during
message interrupt) corrupted shared instance-level flags:
- streamedAssistantTextThisTurn
- streamedReasoningThisTurn

This caused Turn 2 to reset flags while Turn 1 was still reading them,
leading to garbled text or suppressed responses.

Fix:
- Created TurnContext interface to track per-turn streaming state
- Moved flags from instance variables to turn-local context
- Pass TurnContext through translateMessageToEvents, mapBlocksToTimeline,
  and mapPartialEvent call chains

Also fixed:
- session.ts interruptAgentIfRunning() now polls for agent to become
  fully idle (not just cancelled) before starting new run, matching
  the fix from MCP handler

Test:
- Added E2E test that sends overlapping messages and verifies Turn 2
  responds with its own content, not Turn 1's

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Mohamed Boudra
2025-12-25 20:43:42 +07:00
co-authored by Claude Opus 4.5
parent 5a7d16b10f
commit 7e9e685a14
4 changed files with 316 additions and 42 deletions
+52 -21
View File
@@ -81,32 +81,63 @@ Build a new Codex MCP provider side‑by‑side with the existing Codex SDK prov
## Tasks
- [ ] **BUG (CRITICAL)**: Claude agent race condition in `forwardPromptEvents` causes garbled text.
- [x] **BUG (CRITICAL)**: Claude agent race condition in `forwardPromptEvents` causes garbled text.
- **Done (2025-12-25 20:42)**: Fixed race condition by moving instance-level streaming flags to turn-local context.
**ROOT CAUSE**: Race condition in `claude-agent.ts` when `stream()` calls overlap.
**WHAT**:
1. Created `TurnContext` interface (`claude-agent.ts:48-56`) to track per-turn streaming state
2. Removed instance variables `streamedAssistantTextThisTurn` and `streamedReasoningThisTurn` (`claude-agent.ts:368-369`)
3. Modified `forwardPromptEvents()` (`claude-agent.ts:779-785`) to create a turn-local context
4. Updated `translateMessageToEvents()` (`claude-agent.ts:818`) to accept and pass `TurnContext`
5. Updated `mapBlocksToTimeline()` (`claude-agent.ts:1122-1132`) to use `turnContext` for flag tracking
6. Updated `mapPartialEvent()` (`claude-agent.ts:1373`) to pass `turnContext` through
**The Bug** (3 factors):
1. **Fire-and-forget async** (`claude-agent.ts:480`): `this.forwardPromptEvents(...).catch(...)` NOT awaited
2. **Shared mutable state** (`claude-agent.ts:368-369`): `streamedAssistantTextThisTurn` and `streamedReasoningThisTurn` are instance variables
3. **Flag corruption**: Turn 2 resets flags (line 769-770) while Turn 1 is still reading them (line 824-825)
**ALSO FIXED**:
- `session.ts:341-385`: `interruptAgentIfRunning()` now waits for agent to become fully idle (not just cancelled) before starting new run - mirrors fix from MCP handler
**Why E2E Tests Pass**: Sequential messages, each awaited. App/agent-control sends rapid overlapping messages.
**TEST**:
- Added E2E test `daemon.e2e.test.ts:2030-2233` "interrupting message should produce coherent text"
- Test sends message 1 (500 word essay), immediately interrupts with message 2 ("Hello world")
- Before fix: Test failed - message 2 got message 1's response due to flag corruption
- After fix: Test passes - message 2 correctly responds with "Hello world from interrupted message"
**TDD REQUIREMENTS**:
1. **TEST FIRST**: Write E2E test in `daemon.e2e.test.ts`:
- Create Claude agent
- Send message 1 (long prompt like "Write a 500 word essay")
- IMMEDIATELY send message 2 WITHOUT waiting for message 1 (this should interrupt)
- Capture `assistant_message` chunks from message 2
- Assert: chunks are coherent, no garbled/missing text
- This test MUST FAIL before the fix
2. **FIX**: Move flags from instance vars to local vars in `forwardPromptEvents()`:
- Delete lines 368-369 (instance vars)
- Add local vars at start of `forwardPromptEvents()` (line ~769)
- Pass flags through to suppression logic (line 824-825)
3. **VERIFY**: Test passes, typecheck passes, manual verification in app
**VERIFICATION**:
- `npm run typecheck` passes
- All 29 daemon E2E tests pass
- Race condition E2E test specifically validates interrupt handling
**Files**: `packages/server/src/server/agent/providers/claude-agent.ts:368-369, 480, 769-770, 824-825, 1123, 1138, 1149`
- [ ] **FIX (App)**: Complete new agent page (`/agent/new`) - fix bugs, add missing features, test with Playwright.
**Context**: New agent page replaced old modal but left incomplete. Critical bugs and missing features.
**CRITICAL BUGS** (from `REPORT-new-agent-page-review.md`):
1. Image attachments silently fail (`new.tsx:216-218`) - remove early return, wire images to createAgent
2. Creation failures silently ignored (`new.tsx:269-271`) - add error display
3. No error/loading states - add errorMessage and isLoading states
**MISSING FEATURES**:
- Git Options Section (~200 lines in old modal): base branch, new branch, worktree
- Daemon offline error handling
**CLEANUP**:
- Remove dead `CreateAgentModal` code from `home-footer.tsx` (lines 25, 206-209)
**TDD WITH PLAYWRIGHT**:
1. Write Playwright tests FIRST in `packages/app/e2e/` that verify:
- Agent creation with text prompt works
- Agent creation with image attachment works
- Error message shows when creation fails
- Loading state disables button during creation
- Git options appear for git directory
2. Run tests - should FAIL initially
3. Fix bugs and add features
4. Run tests - should PASS
5. Compare with `create-agent-modal.tsx` for feature parity
**Files**:
- `packages/app/src/app/agent/new.tsx` - Fix this
- `packages/app/src/components/create-agent-modal.tsx` - Reference
- `packages/app/src/components/home-footer.tsx` - Dead code cleanup
- [x] **BUG (MCP)**: `send_agent_prompt` errors when agent already running.
- **Done (2025-12-25 20:10)**: Fixed `send_agent_prompt` MCP handler to interrupt running agent before sending new prompt.