Skip to content

fix(wire): reject steer messages after turn ends#1591

Open
howardpen9 wants to merge 1 commit intoMoonshotAI:mainfrom
howardpen9:fix/steer-race-conditions
Open

fix(wire): reject steer messages after turn ends#1591
howardpen9 wants to merge 1 commit intoMoonshotAI:mainfrom
howardpen9:fix/steer-race-conditions

Conversation

@howardpen9
Copy link
Copy Markdown
Contributor

@howardpen9 howardpen9 commented Mar 26, 2026

Summary

  • Fix a race condition in WireServer._handle_steer() where steer messages sent after run_soul() returns but before _cancel_event cleanup would be accepted with status=steered but never consumed — silently lost
  • Add _turn_active flag to precisely track soul execution lifecycle, replacing the _is_streaming check (which relied on _cancel_event is not None) for steer gating
  • Add test covering the tail-window race scenario

Problem

_is_streaming is derived from _cancel_event is not None. Since _cancel_event is only cleared in the finally block of _handle_prompt(), there's a window between run_soul() returning and the finally cleanup where:

  1. Client receives TurnEnd event
  2. Client sends a steer message
  3. _handle_steer() sees _is_streaming == True and accepts it
  4. soul.steer() queues the message
  5. But the turn is already over — the steer is never consumed
  6. At next turn start, _agent_loop() discards it as "stale"

Since _read_loop() dispatches inbound JSON-RPC messages concurrently via asyncio.create_task, the ordering between _handle_prompt's finally block and _handle_steer is scheduler-dependent.

Fix

Add a _turn_active: bool flag that is:

  • Set to True when _handle_prompt() starts run_soul()
  • Set to False immediately when run_soul() returns (before finally cleanup)

_handle_steer() now checks _turn_active instead of _is_streaming, closing the tail window.

Test plan

  • New test: test_handle_steer_rejects_after_turn_ends — simulates the exact race window
  • Existing test updated: test_handle_steer_queues_input_when_streaming — sets _turn_active = True
  • All 6 wire server steer tests pass
  • All 12 kimisoul steer tests pass

Open with Devin

…ssage loss

The WireServer used `_is_streaming` (based on `_cancel_event is not None`)
to gate steer acceptance. Since `_cancel_event` is only cleared in the
`finally` block of `_handle_prompt()`, there was a window after
`run_soul()` returns but before cleanup where steers would be accepted
with `status=steered` but never consumed — silently lost.

This adds a `_turn_active` flag that tracks the actual soul execution
lifecycle, set to True at turn start and False immediately when
`run_soul()` returns. `_handle_steer()` now checks `_turn_active`
instead of `_is_streaming`.
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 2 additional findings.

Open in Devin Review

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