Skip to content

fix: handle turn.aborted in ProviderRuntimeIngestion to unblock Copilot stop#16

Merged
zortos293 merged 3 commits intomainfrom
fix/copilot-stop-button
Mar 11, 2026
Merged

fix: handle turn.aborted in ProviderRuntimeIngestion to unblock Copilot stop#16
zortos293 merged 3 commits intomainfrom
fix/copilot-stop-button

Conversation

@zortos293
Copy link
Copy Markdown
Owner

@zortos293 zortos293 commented Mar 11, 2026

Problem

When the user clicks the stop button during a Copilot turn, CopilotAdapter calls session.abort() on the Copilot SDK which emits a turn.aborted runtime event. However, ProviderRuntimeIngestion only handled turn.completed for lifecycle transitions — leaving the session stuck in "running" with a stale activeTurnId, which blocked all subsequent sends.

Fix

Added turn.aborted alongside turn.completed in four lifecycle handling paths within ProviderRuntimeIngestion.ts:

  1. shouldApplyThreadLifecycle guard — allows the abort event through the strict lifecycle filter
  2. Lifecycle event condition & activeTurnId clearing — clears the active turn on abort
  3. Status derivation — maps turn.aborted"interrupted" (which the UI maps to "ready" phase, re-enabling the input)
  4. Turn finalization — finalizes buffered assistant messages and proposed plans on abort

Tests

Two new tests in ProviderRuntimeIngestion.test.ts:

  • turn.aborted transitions session to "interrupted" with activeTurnId cleared
  • After abort, a new turn.started successfully transitions back to "running"

All 25 tests pass. Typecheck and lint clean.

Summary by CodeRabbit

  • New Features
    • Handle aborted turns by marking sessions as interrupted, clearing the active turn, and allowing sessions to resume and accept new turns.
    • Treat aborted turns like completed ones for lifecycle, finalization, and error-state handling.
  • Tests
    • Added tests for aborted-turn mapping, session recovery after abort, and ignoring late aborts for non-active turns.

…ot stop

When the user clicks stop during a Copilot turn, CopilotAdapter emits a
turn.aborted runtime event. ProviderRuntimeIngestion only handled
turn.completed for lifecycle transitions, leaving the session stuck in
"running" with a stale activeTurnId — blocking new sends.

Add turn.aborted to the lifecycle event handling so it:
- Transitions session status to "interrupted" (maps to "ready" in UI)
- Clears activeTurnId to null
- Finalizes buffered assistant messages and proposed plans

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 11, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 65d62db2-328d-4fda-bc15-3c3e9774e115

📥 Commits

Reviewing files that changed from the base of the PR and between 22c61b8 and f32e562.

📒 Files selected for processing (2)
  • apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts
  • apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts

📝 Walkthrough

Walkthrough

This change introduces handling for a new turn.aborted event in the provider runtime ingestion orchestration layer. The implementation treats turn.aborted similarly to turn.completed for lifecycle guard decisions, updating the active turn identifier and session status accordingly. The event maps to an "interrupted" session status and clears the active turn context. Test coverage is added to verify that the session transitions to interrupted state following a turn.aborted event and can successfully resume to running state when a new turn starts.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: handling turn.aborted events in ProviderRuntimeIngestion to fix a blocking issue with Copilot's stop button.
Description check ✅ Passed The description provides a clear problem statement, detailed explanation of the fix across four lifecycle paths, and mentions test coverage with all tests passing.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts (1)

852-863: ⚠️ Potential issue | 🟠 Major

Gate turn.aborted on the current active turn.

turn.aborted is now using the turn.completed fallback path, so a late/duplicate abort with no active turn can still move the session to "interrupted" and clear lastError. Also, the new finalization block does not consult the lifecycle guard, so an abort for a different turn can still flush buffered assistant text / proposed plans from that stale turn. Please restrict abort handling to a matching activeTurnId, and add a regression for late/different-turn aborts.

Possible tightening
-          case "turn.completed":
-          case "turn.aborted":
+          case "turn.completed":
             if (conflictsWithActiveTurn || missingTurnForActiveTurn) {
               return false;
             }
             // Only the active turn may close the lifecycle state.
             if (activeTurnId !== null && eventTurnId !== undefined) {
               return sameId(activeTurnId, eventTurnId);
             }
             // If no active turn is tracked, accept completion scoped to this thread.
             return true;
+          case "turn.aborted":
+            return (
+              activeTurnId !== null &&
+              eventTurnId !== undefined &&
+              sameId(activeTurnId, eventTurnId)
+            );
-      if (event.type === "turn.completed" || event.type === "turn.aborted") {
+      const shouldFinalizeTurn =
+        event.type === "turn.completed" ||
+        (event.type === "turn.aborted" &&
+          activeTurnId !== null &&
+          eventTurnId !== undefined &&
+          sameId(activeTurnId, eventTurnId));
+      if (shouldFinalizeTurn) {

As per coding guidelines, apps/server/**: Prioritize correctness, reliability, and predictable behavior under reconnects, session restarts, partial streams, and provider failures.

Also applies to: 1044-1075

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts` around
lines 852 - 863, The handling for "turn.aborted" currently falls through to the
"turn.completed" path allowing late or out-of-thread aborts to clear session
state and flush stale assistant content; change the switch so "turn.aborted" is
explicitly gated to only succeed when activeTurnId is non-null and
sameId(activeTurnId, eventTurnId) is true (i.e., mirror the explicit active-turn
check used for completions) and ensure the finalization block consults the
lifecycle guard before clearing lastError or flushing buffered assistant
text/proposed plans; update ProviderRuntimeIngestion's turn finalization logic
(the block referencing activeTurnId, eventTurnId, sameId, and the lifecycle
guard) and add a regression test that sends a late/different-turn "turn.aborted"
to assert that session state, lastError, and buffered assistant output remain
unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts`:
- Around line 852-863: The handling for "turn.aborted" currently falls through
to the "turn.completed" path allowing late or out-of-thread aborts to clear
session state and flush stale assistant content; change the switch so
"turn.aborted" is explicitly gated to only succeed when activeTurnId is non-null
and sameId(activeTurnId, eventTurnId) is true (i.e., mirror the explicit
active-turn check used for completions) and ensure the finalization block
consults the lifecycle guard before clearing lastError or flushing buffered
assistant text/proposed plans; update ProviderRuntimeIngestion's turn
finalization logic (the block referencing activeTurnId, eventTurnId, sameId, and
the lifecycle guard) and add a regression test that sends a late/different-turn
"turn.aborted" to assert that session state, lastError, and buffered assistant
output remain unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d5662301-8d6e-451a-9ae8-9080d8f3691e

📥 Commits

Reviewing files that changed from the base of the PR and between d97cb3a and 22c61b8.

📒 Files selected for processing (2)
  • apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts
  • apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts

A late or duplicate turn.aborted for a stale turn could incorrectly
transition the session to 'interrupted' and flush buffered assistant
messages from the wrong turn. Gate the finalization block with
shouldApplyThreadLifecycle (which verifies the event's turnId matches
the active turn). Add regression test for stale-turn abort rejection.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@zortos293 zortos293 merged commit 6ddb106 into main Mar 11, 2026
3 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