Skip to content

fix(server): cancel pending questions on stop - #7987

Open
Exotic209093 wants to merge 2 commits into
pingdotgg:mainfrom
Exotic209093:fix/stop-dismiss-pending-questions
Open

fix(server): cancel pending questions on stop#7987
Exotic209093 wants to merge 2 commits into
pingdotgg:mainfrom
Exotic209093:fix/stop-dismiss-pending-questions

Conversation

@Exotic209093

@Exotic209093 Exotic209093 commented Aug 23, 2026

Copy link
Copy Markdown

Problem

Stopping a turn that is blocked on a user question left the question prompt stuck on screen. The UI cleared it optimistically, but nothing durable recorded the resolution, so a reload or another client brought the dead prompt back and any answer to it failed as stale.

Fix

After a successful provider interrupt, the ProviderCommandReactor re-reads the thread's activity log and derives the still-open user-input requests from that fresh snapshot (honoring prior resolutions and stale-response failures), so a resolution the provider appends while handling the interrupt is never followed by a false cancelled duplicate. Each durable user-input.resolved activity marked cancelled: true is stamped with server-side time rather than the stop command's timestamp, so it orders after the request activity even when the stop command carries an older one. The pending-UI derivation in every client then clears the prompt from the event stream itself, with no client-side special casing.

The reactor test covers an open request being cancelled despite an interrupt timestamp older than the request, a question the provider resolved during the interrupt keeping its real resolution untouched, and already-answered or stale-failed requests staying that way.

  • bb49562 cancels pending questions after interrupt
  • de3647d extends the test coverage

Note

Medium Risk
Touches the turn-interrupt path and writes new orchestration activities. Wrong pending-request derivation could cancel live questions or leave dead prompts in the log.

Overview
Stopping a turn that is waiting on a user question now records a durable cancellation so the prompt does not come back after reload or on another client.

After a successful provider interrupt, ProviderCommandReactor walks the thread activity log, finds still-open user-input requests (skipping already-resolved ones and stale-response failures), and appends user-input.resolved activities with cancelled: true. Clients already treat that kind as closed, so the prompt clears from the event stream without extra UI logic.

Reviewed by Cursor Bugbot for commit b6e1edf. Configure here.

Note

Cancel pending user-input requests on thread.turn.interrupt in ProviderCommandReactor

  • On thread.turn.interrupt-requested, after calling providerService.interruptTurn, the reactor now appends user-input.resolved activities with {cancelled: true} for any still-open user-input requests inferred from the activity log.
  • Adds pendingUserInputRequests to scan sorted thread activities and compute open request IDs, closing them on resolution or when provider.user-input.respond.failed marks the request as stale or unknown.
  • Adds appendCancelledUserInputActivity to append the cancellation activity with summary "User input cancelled", preserving the original turnId and using the interrupt event's createdAt.
  • Behavioral Change: previously-open user-input requests are now resolved as cancelled on interrupt; consumers reading user-input.resolved activities in ProviderCommandReactor.ts should handle the new {cancelled: true} payload.
📊 Macroscope summarized de3647d. 1 file reviewed, 2 issues evaluated, 0 issues filtered, 2 comments posted

🗂️ Filtered Issues

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Aug 23, 2026
@coderabbitai

coderabbitai Bot commented Aug 23, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7f4a0cd4-2f2d-4971-8ba1-fada9a9895d5

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b6e1edf330

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1267 to +1268
yield* Effect.forEach(pendingUserInputRequests(thread.activities), ({ requestId, turnId }) =>
appendCancelledUserInputActivity({

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Cancel questions in the session-stop path

This cleanup runs only for thread.turn-interrupt-requested. When thread.session.stop is dispatched directly—such as archive/settle cleanup or the branch selector—the event instead reaches processSessionStopRequested, which stops the provider and marks the session stopped without appending any user-input.resolved activity. If the session was blocked on a question, reloading or later reopening the thread therefore still restores the now-unanswerable prompt. Apply the same durable cancellation cleanup after a successful session stop.

Useful? React with 👍 / 👎.

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit b6e1edf. Configure here.

turnId,
createdAt: event.payload.createdAt,
}),
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Stale snapshot double-cancels

Low Severity

pendingUserInputRequests runs against the thread snapshot taken before interruptTurn. Claude and Cursor already settle open prompts and emit user-input.resolved during interrupt, so the reactor still appends a second user-input.resolved with cancelled: true for requests that are no longer open.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b6e1edf. Configure here.

@macroscopeapp

macroscopeapp Bot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Skipped

Macroscope did not run approvability analysis for this PR. Macroscope could not determine whether this PR modifies its approvability configuration, so the PR was not approved automatically. A PR that may change the rules that govern approval is never approved automatically.

Not approved because:

  • 2 blocking correctness issues found at or above your repo's Minimum Blocking Severity

@Exotic209093
Exotic209093 force-pushed the fix/stop-dismiss-pending-questions branch from b6e1edf to 37eaf59 Compare August 23, 2026 10:02
The stop test now verifies an already-answered question and one whose
response failed as stale are left alone, a question the provider resolves
during the interrupt keeps its real resolution without a cancelled
duplicate on top, and a still-open question gets a durable cancelled
resolution stamped after the request even when the interrupt command
predates it. Dispatches run in one Effect.gen so the file stays under the
manual-effect-runner lint baseline.
@Exotic209093
Exotic209093 force-pushed the fix/stop-dismiss-pending-questions branch from 37eaf59 to de3647d Compare August 23, 2026 10:03
// while handling the interrupt. Cancellations get server-side timestamps
// so they order after the request activity even when the interrupt
// command predates it.
const postInterruptThread = yield* resolveThread(event.payload.threadId);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Medium Layers/ProviderCommandReactor.ts:1276

The interrupt path can append a user-input.resolved cancellation before the provider's real resolution, leaving a false cancellation and duplicate resolution in the durable activity log. resolveThread at this point may run before the callback fiber resumes and ProviderRuntimeIngestion projects the resolution; coordinate with callback completion or make cancellation resolution idempotent.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/orchestration/Layers/ProviderCommandReactor.ts around line 1276:

The interrupt path can append a `user-input.resolved` cancellation before the provider's real resolution, leaving a false cancellation and duplicate resolution in the durable activity log. `resolveThread` at this point may run before the callback fiber resumes and `ProviderRuntimeIngestion` projects the resolution; coordinate with callback completion or make cancellation resolution idempotent.

activities: OrchestrationThread["activities"],
): ReadonlyArray<{ readonly requestId: string; readonly turnId: TurnId | null }> {
const openRequests = new Map<string, TurnId | null>();
const ordered = [...activities].toSorted(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Medium Layers/ProviderCommandReactor.ts:288

When user-input.requested and user-input.resolved share a createdAt timestamp, an ID ordering where the resolution sorts first causes pendingUserInputRequests to delete the request and then reopen it. The function therefore returns an already-resolved question as pending, so an interrupt appends a false cancellation. Use a durable append sequence/order for equal-timestamp lifecycle events instead of activity.id.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/orchestration/Layers/ProviderCommandReactor.ts around line 288:

When `user-input.requested` and `user-input.resolved` share a `createdAt` timestamp, an ID ordering where the resolution sorts first causes `pendingUserInputRequests` to delete the request and then reopen it. The function therefore returns an already-resolved question as pending, so an interrupt appends a false cancellation. Use a durable append sequence/order for equal-timestamp lifecycle events instead of `activity.id`.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M 30-99 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant