You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
After the first turn completes, sending a second message in the same conversation makes the session permanently stuck in a "busy" state: the input is blocked, the pending queue never drains, and the only workaround is creating a new conversation. This is a regression introduced by PR #2372 and is still present on the latest main (2ef884e, which includes 405c1c7). A brand-new conversation works fine for its first message; the bug reliably reproduces on the second message of any conversation.
Area
Agent runtime / core
Reproduction or evidence
Environment: official upstream main at 2ef884e (includes PR #2372 merge 405c1c7), Windows 11, Desktop app.
Steps:
Open a new conversation.
Send a message and wait for the first turn to complete normally.
Send a second message in the same conversation.
The session stays busy forever: message never dispatched, subsequent sends blocked, pending queue never drains. Creating a new conversation is the only recovery.
Root cause chain (references on the 405c1c7 tree):
Frontend state machine settlement depends solely on DialogTurnCompleted + a 500 ms timer. finalizeTurnCompletionState is the only path that issues FINISHING_SETTLED -> IDLE (EventHandlerModule.ts:1110-1207, transitions.ts:26/47). The backend does not emit SessionStateChanged(Idle) on normal completion (only recovered-processing and cancel paths, coordinator.rs:7036/7526), so the frontend resets 500 ms after DialogTurnCompleted — regardless of whether the backend finished persisting.
PR #2372 made the backend completion path hold the session mutation lock for disk I/O. complete_dialog_turn now calls acquire_session_mutation(session_id) (session_manager.rs:7482) and, while holding it, runs load_dialog_turn + merge + save_dialog_turn (the same keyed lock also serializes dialog-turn starts — comment at session_manager.rs:7475-7481). The PR itself acknowledges this can hold session coordination for seconds with large snapshot indexes (PR #2372 Motivation). The backend SessionState::Idle reset (coordinator.rs:2990-3007) therefore happens noticeably later than the frontend's fixed 500 ms reset.
The widened frontend-IDLE / backend-Processing window makes the second submission hit the backend rejection. The frontend busy gate only consults the local state machine (MessageModule.ts:243-246), so once the frontend is IDLE the user can submit. LocalSessionDriver.startTurn transitions START -> PROCESSING before the RPC (LocalSessionDriver.ts:365 -> :452), then the backend rejects because the session is still Processing (coordinator.rs:5761-5787: Err("Session state does not allow starting new dialog")), or silently queues (scheduler.rs:1561-1577, DialogSubmitOutcome::Queued with no event).
The official catch path does not reliably reset the machine on this rejection. The reset condition is ownsProcessingTurn = createdLocalTurnId === activeDialogTurnId (MessageModule.ts:417-429), which does not cover every rejected/queued path. When the machine stays in PROCESSING, every subsequent send is blocked by the busy gate and the pending queue never drains (drain gated on IDLE, MessageModule.ts:632-641) -> permanent busy hang.
Additionally, eventOwnsLatestSessionTurn (EventHandlerModule.ts:183-192) can suppress settlement entirely when the previous turn's DialogTurnCompleted arrives after a newer turn was appended (multi-turn conversations): BACKEND_STREAM_COMPLETED and beginTurnCompletion are both skipped (:2648/:2658) and FINISHING_SETTLED is never issued, leaving the machine in PROCESSING permanently.
Why the first message is unaffected: on first submission the backend is guaranteed IDLE (fresh session), so the normal event flow runs end to end.
Regression attribution: git log 405c1c7..upstream/main contains only 5 unrelated harmonyos/IME commits — no fix has landed since. Before #2372 the same frontend interception logic existed but the backend completion path was lock-free and fast, so the race window was practically unreachable.
Environment, if relevant
BitFun commit: 2ef884e (upstream main, includes PR #2372405c1c7)
OS: Windows 11
Mode: Desktop app, agentic session
Area
Desktop app
Reproduction or evidence
Environment: official upstream main at 2ef884e (includes PR #2372 merge 405c1c7), Windows 11, Desktop app.
Steps:
Open a new conversation.
Send a message and wait for the first turn to complete normally.
Send a second message in the same conversation.
The session stays busy forever: message never dispatched, subsequent sends blocked, pending queue never drains. Creating a new conversation is the only recovery.
Root cause chain (references on the 405c1c7 tree):
Frontend state machine settlement depends solely on DialogTurnCompleted + a 500 ms timer. finalizeTurnCompletionState is the only path that issues FINISHING_SETTLED -> IDLE (EventHandlerModule.ts:1110-1207, transitions.ts:26/47). The backend does not emit SessionStateChanged(Idle) on normal completion (only recovered-processing and cancel paths, coordinator.rs:7036/7526), so the frontend resets 500 ms after DialogTurnCompleted — regardless of whether the backend finished persisting.
PR fix(sessions): stabilize durable history reconciliation #2372 made the backend completion path hold the session mutation lock for disk I/O. complete_dialog_turn now calls acquire_session_mutation(session_id) (session_manager.rs:7482) and, while holding it, runs load_dialog_turn + merge + save_dialog_turn (the same keyed lock also serializes dialog-turn starts — comment at session_manager.rs:7475-7481). The PR itself acknowledges this can hold session coordination for seconds with large snapshot indexes (PR fix(sessions): stabilize durable history reconciliation #2372 Motivation). The backend SessionState::Idle reset (coordinator.rs:2990-3007) therefore happens noticeably later than the frontend's fixed 500 ms reset.
The widened frontend-IDLE / backend-Processing window makes the second submission hit the backend rejection. The frontend busy gate only consults the local state machine (MessageModule.ts:243-246), so once the frontend is IDLE the user can submit. LocalSessionDriver.startTurn transitions START -> PROCESSING before the RPC (LocalSessionDriver.ts:365 -> :452), then the backend rejects because the session is still Processing (coordinator.rs:5761-5787: Err("Session state does not allow starting new dialog")), or silently queues (scheduler.rs:1561-1577, DialogSubmitOutcome::Queued with no event).
The official catch path does not reliably reset the machine on this rejection. The reset condition is ownsProcessingTurn = createdLocalTurnId === activeDialogTurnId (MessageModule.ts:417-429), which does not cover every rejected/queued path. When the machine stays in PROCESSING, every subsequent send is blocked by the busy gate and the pending queue never drains (drain gated on IDLE, MessageModule.ts:632-641) -> permanent busy hang.
Additionally, eventOwnsLatestSessionTurn (EventHandlerModule.ts:183-192) can suppress settlement entirely when the previous turn's DialogTurnCompleted arrives after a newer turn was appended (multi-turn conversations): BACKEND_STREAM_COMPLETED and beginTurnCompletion are both skipped (:2648/:2658) and FINISHING_SETTLED is never issued, leaving the machine in PROCESSING permanently.
Why the first message is unaffected: on first submission the backend is guaranteed IDLE (fresh session), so the normal event flow runs end to end.
Regression attribution: git log 405c1c7..upstream/main contains only 5 unrelated harmonyos/IME commits — no fix has landed since. Before #2372 the same frontend interception logic existed but the backend completion path was lock-free and fast, so the race window was practically unreachable.
Summary
Summary
After the first turn completes, sending a second message in the same conversation makes the session permanently stuck in a "busy" state: the input is blocked, the pending queue never drains, and the only workaround is creating a new conversation. This is a regression introduced by PR #2372 and is still present on the latest main (2ef884e, which includes 405c1c7). A brand-new conversation works fine for its first message; the bug reliably reproduces on the second message of any conversation.
Area
Agent runtime / core
Reproduction or evidence
Environment: official upstream main at 2ef884e (includes PR #2372 merge 405c1c7), Windows 11, Desktop app.
Steps:
Open a new conversation.
Send a message and wait for the first turn to complete normally.
Send a second message in the same conversation.
The session stays busy forever: message never dispatched, subsequent sends blocked, pending queue never drains. Creating a new conversation is the only recovery.
Root cause chain (references on the 405c1c7 tree):
Frontend state machine settlement depends solely on DialogTurnCompleted + a 500 ms timer. finalizeTurnCompletionState is the only path that issues FINISHING_SETTLED -> IDLE (EventHandlerModule.ts:1110-1207, transitions.ts:26/47). The backend does not emit SessionStateChanged(Idle) on normal completion (only recovered-processing and cancel paths, coordinator.rs:7036/7526), so the frontend resets 500 ms after DialogTurnCompleted — regardless of whether the backend finished persisting.
PR #2372 made the backend completion path hold the session mutation lock for disk I/O. complete_dialog_turn now calls acquire_session_mutation(session_id) (session_manager.rs:7482) and, while holding it, runs load_dialog_turn + merge + save_dialog_turn (the same keyed lock also serializes dialog-turn starts — comment at session_manager.rs:7475-7481). The PR itself acknowledges this can hold session coordination for seconds with large snapshot indexes (PR #2372 Motivation). The backend SessionState::Idle reset (coordinator.rs:2990-3007) therefore happens noticeably later than the frontend's fixed 500 ms reset.
The widened frontend-IDLE / backend-Processing window makes the second submission hit the backend rejection. The frontend busy gate only consults the local state machine (MessageModule.ts:243-246), so once the frontend is IDLE the user can submit. LocalSessionDriver.startTurn transitions START -> PROCESSING before the RPC (LocalSessionDriver.ts:365 -> :452), then the backend rejects because the session is still Processing (coordinator.rs:5761-5787: Err("Session state does not allow starting new dialog")), or silently queues (scheduler.rs:1561-1577, DialogSubmitOutcome::Queued with no event).
The official catch path does not reliably reset the machine on this rejection. The reset condition is ownsProcessingTurn = createdLocalTurnId === activeDialogTurnId (MessageModule.ts:417-429), which does not cover every rejected/queued path. When the machine stays in PROCESSING, every subsequent send is blocked by the busy gate and the pending queue never drains (drain gated on IDLE, MessageModule.ts:632-641) -> permanent busy hang.
Additionally, eventOwnsLatestSessionTurn (EventHandlerModule.ts:183-192) can suppress settlement entirely when the previous turn's DialogTurnCompleted arrives after a newer turn was appended (multi-turn conversations): BACKEND_STREAM_COMPLETED and beginTurnCompletion are both skipped (:2648/:2658) and FINISHING_SETTLED is never issued, leaving the machine in PROCESSING permanently.
Why the first message is unaffected: on first submission the backend is guaranteed IDLE (fresh session), so the normal event flow runs end to end.
Regression attribution: git log 405c1c7..upstream/main contains only 5 unrelated harmonyos/IME commits — no fix has landed since. Before #2372 the same frontend interception logic existed but the backend completion path was lock-free and fast, so the race window was practically unreachable.
Environment, if relevant
BitFun commit: 2ef884e (upstream main, includes PR #2372 405c1c7)
OS: Windows 11
Mode: Desktop app, agentic session
Area
Desktop app
Reproduction or evidence
Environment: official upstream main at 2ef884e (includes PR #2372 merge 405c1c7), Windows 11, Desktop app.
Steps:
Root cause chain (references on the 405c1c7 tree):
Frontend state machine settlement depends solely on DialogTurnCompleted + a 500 ms timer. finalizeTurnCompletionState is the only path that issues FINISHING_SETTLED -> IDLE (EventHandlerModule.ts:1110-1207, transitions.ts:26/47). The backend does not emit SessionStateChanged(Idle) on normal completion (only recovered-processing and cancel paths, coordinator.rs:7036/7526), so the frontend resets 500 ms after DialogTurnCompleted — regardless of whether the backend finished persisting.
PR fix(sessions): stabilize durable history reconciliation #2372 made the backend completion path hold the session mutation lock for disk I/O. complete_dialog_turn now calls acquire_session_mutation(session_id) (session_manager.rs:7482) and, while holding it, runs load_dialog_turn + merge + save_dialog_turn (the same keyed lock also serializes dialog-turn starts — comment at session_manager.rs:7475-7481). The PR itself acknowledges this can hold session coordination for seconds with large snapshot indexes (PR fix(sessions): stabilize durable history reconciliation #2372 Motivation). The backend SessionState::Idle reset (coordinator.rs:2990-3007) therefore happens noticeably later than the frontend's fixed 500 ms reset.
The widened frontend-IDLE / backend-Processing window makes the second submission hit the backend rejection. The frontend busy gate only consults the local state machine (MessageModule.ts:243-246), so once the frontend is IDLE the user can submit. LocalSessionDriver.startTurn transitions START -> PROCESSING before the RPC (LocalSessionDriver.ts:365 -> :452), then the backend rejects because the session is still Processing (coordinator.rs:5761-5787: Err("Session state does not allow starting new dialog")), or silently queues (scheduler.rs:1561-1577, DialogSubmitOutcome::Queued with no event).
The official catch path does not reliably reset the machine on this rejection. The reset condition is ownsProcessingTurn = createdLocalTurnId === activeDialogTurnId (MessageModule.ts:417-429), which does not cover every rejected/queued path. When the machine stays in PROCESSING, every subsequent send is blocked by the busy gate and the pending queue never drains (drain gated on IDLE, MessageModule.ts:632-641) -> permanent busy hang.
Additionally, eventOwnsLatestSessionTurn (EventHandlerModule.ts:183-192) can suppress settlement entirely when the previous turn's DialogTurnCompleted arrives after a newer turn was appended (multi-turn conversations): BACKEND_STREAM_COMPLETED and beginTurnCompletion are both skipped (:2648/:2658) and FINISHING_SETTLED is never issued, leaving the machine in PROCESSING permanently.
Why the first message is unaffected: on first submission the backend is guaranteed IDLE (fresh session), so the normal event flow runs end to end.
Regression attribution: git log 405c1c7..upstream/main contains only 5 unrelated harmonyos/IME commits — no fix has landed since. Before #2372 the same frontend interception logic existed but the backend completion path was lock-free and fast, so the race window was practically unreachable.
Environment, if relevant
No response