Skip to content

fix(orchestrator): Harden Grok v2 settlement and steer message visibility#3578

Open
mwolson wants to merge 2 commits into
pingdotgg:t3code/codex-turn-mappingfrom
mwolson:fix/grok-v2
Open

fix(orchestrator): Harden Grok v2 settlement and steer message visibility#3578
mwolson wants to merge 2 commits into
pingdotgg:t3code/codex-turn-mappingfrom
mwolson:fix/grok-v2

Conversation

@mwolson

@mwolson mwolson commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Fixes #3580

What you'll notice

Steered messages stay visible

When you steer an in-flight Grok run, your message no longer flashes away between server events. The timeline keeps the optimistic user row until the committed user_message turn item lands.

Grok runs finish instead of wedging on Working

Grok root turns that already streamed a full reply could stay running when the provider's session/prompt RPC never returned. The next message was then auto-routed as a steer (restart_active), which felt like the app ignored what you typed.

This PR terminalizes those root turns from root-session streaming (debounced idle settle) instead of treating xAI subagent prompt_complete notifications as root completion. The next message starts a normal turn.

Stacks on t3code/codex-turn-mapping (#2829 orchestrator v2 integration).

This PR only: two commits on fix/grok-v2 — (1) apps/web steer visibility (ChatView.tsx, ChatView.logic.ts); (2) Grok settlement (AcpAdapterV2.ts, GrokAdapterV2.ts, XAiAcpExtension.ts, tests). Everything else comes from the base branch.

User-facing problems and fixes

What you saw Why Fix
Steered user text vanished briefly (or until refresh) on the v2 timeline message.updated arrived before turn-item.updated; optimistic rows were evicted when projection.messages contained the id, but rendering uses visibleTurnItems. Derive committed user ids from visible user_message turn items (deriveCommittedServerUserMessageIds).
Run stayed on Working after Grok finished replying; next send felt like a mis-steer Stranded root session/prompt left the provider turn running; client restart policy kicked in. Root-session-id-gated _x.ai/session/prompt_complete racing in XAiAcpExtension (ignore foreign/subagent session ids) plus debounced idle settle when assistant output is already ingested.
Assistant preamble appeared, then the run stopped before tools ran Idle settle fired after the first assistant chunk on a tool turn; runtime.cancel interrupted the hung session/prompt while tools were still in flight. Gate idle settle when tool history or a running subagent exists (hasToolHistory, hasRunningSubagent in acpRootTurnIsIdle).

Server-side mechanics (same Grok wedge as above)

Condition Mitigation
Grok keepalive notifications (~74 B every ~2s) kept resetting idle-settle debounce, so hung prompts never terminalized Only count ingestible root output for activity and settle re-arm (acpRootSessionUpdateIngestsOutput); keepalives are ignored.
Idle settle fired after assistant preamble while tools or follow-up text were still in flight Defer idle settle when context.tools is non-empty or a subagent is running; assistant-only turns still use idle settle for hung-prompt recovery.
Assistant-only hung prompt with real output already ingested Require ingested assistant output, ~2s settle debounce, cooperative trailing-chunk yield before finalize (no Effect.sleep drain — replay/TestClock deadlock).
Prompt started but zero root output ever arrived ~30s liveness watchdog fails the turn when there is no ingested output and no pending approval/user-input block.

xAI prompt_complete scope

Per upstream review on the base branch: many prompt_complete notifications are subagent completions, not root-turn completion. We did not port the v1 wholesale race. This PR only resolves root pending prompts when notification.sessionId matches the root entry. Child-session routing for subagent visibility stays on the codex-turn-mapping base.

Review-driven fixes (not from the original repro)

These came from Macroscope/Bugbot review and manual retest of the settlement path. They tighten correctness without changing the user-visible goals above.

Finding Fix
Final assistant/reasoning chunks skipped idle-settle debounce (return before scheduler) break instead of return on chunk handlers.
Quiet assistant stream left current open, blocking idle settle after debounce Close text streams at the start of trySettleRootTurnWhenIdle.
Completed plan artifact left hasActivePlan true forever Remove the hasActivePlan idle guard (running tools + pending requests already gate settle).
Idle settle finalized locally without session/cancel, risking concurrent prompts Call runtime.cancel before idle finalizeTurn.
Pending approval/user-input blocked settle, then never re-armed; watchdog could fail during legitimate waits Re-schedule settle when pending requests clear; skip watchdog while a pending request belongs to the turn.
Watchdog deferred at 30s for pending approval/user-input, then never re-armed after the user responded rearmRootTurnRecoveryTimers re-schedules both idle settle and the liveness watchdog when pending requests clear.
Grok keepalive session/update traffic re-armed settle debounce without ingestible output acpRootSessionUpdateIngestsOutput gates hasRootActivity and settle scheduling to real chunks, tools, and plan updates.
Tool turn settled on assistant preamble before session/prompt RPC completed acpRootTurnIsIdle returns false when tool history or a running subagent exists.

Out of scope (follow-up PRs or upstream)

Topic Why deferred
Reasoning-only turns settling too early or stranded with zero assistant output No stable replay fixture; assistant-visible and tool-gated paths cover the manual repro.
Silent hang with zero ingested output after session/prompt (live chunks never land in T3) Separate ingestion/routing class; needs child-session or load-window investigation, not more settle thresholds.
interruptPromptOnCancel: true for Grok Intentionally falsetrue breaks message_steering/grok replay. User Stop still sends session/cancel; idle settle also calls runtime.cancel.
Bookkeeping session/update beyond keepalives suppressing the liveness watchdog Low risk after keepalive gating; separate if a new repro appears.
Split committedServerMessageIds from env-lock activeMessageCount Brief projection gap only; steer fix intentionally keys off visible turn items.
xAI prompt_complete dedup without promptId; guard cancel from calling start() Pre-existing / low risk under serialized root prompts; not blocking this wedge fix.
Child-session subagent chunk routing via _meta.sessionId Julian/base scope, not this PR.
Effect.forkChildEffect.forkDetach for the hung session/prompt RPC fiber in XAiAcpExtension Review concern: scope interruption can be swallowed as synthetic { stopReason: "cancelled" }. Not the cause of post-completion provider-log Interrupt (that is intentional idle-settle runtime.cancel after finalizeTurn). Revisit as a separate hardening pass if scope-shutdown misclassification shows up in repro.

Validation

  • vp check: pass
  • vp test apps/web/src/components/ChatView.logic.test.ts: pass
  • vp test apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.test.ts: pass
  • vp test apps/server/src/provider/acp/XAiAcpExtension.test.ts: pass
  • vp test apps/server/src/orchestration-v2/testkit/OrchestratorReplayFixtures.integration.test.ts (grok + message_steering/grok): pass
  • Integration AppImage (orchestrator-v2 + this branch @ c64ce4c53): manual pass on Grok session 019f067e-5178-70b1-86b5-34f0ab0f2a40 / T3 thread 93bb87ad-2108-4ea4-9990-45f7056c75a6:
    • Run 5 (turn_start): assistant-only replay, ~15s, full reasoning + assistant in DB
    • Run 6 (turn_start): tool turn (prettier on notes), ~8s, file_search + command_execution + assistant in DB
    • Both runs completed; zero inputIntent: steer in provider log
    • Run 4 on the same thread is pre-fix historical data (preamble-only UX before tool-phase gating)

Known limitations

  • Post-settle Grok CLI keepalive traffic may continue in provider logs after the run is already completed; harmless but noisy.
  • Completed runs may still show session/prompt ending with Interrupt in the provider log (idle settle calls runtime.cancel on the hung RPC after content is already ingested). Runs complete with full content; this is log noise, not the run-4 failure mode.
  • One file_search item failed on a very large notes file in manual testing; Grok recovered and completed normally.

Note

Medium Risk
Changes core Grok/ACP turn lifecycle (finalize, cancel, timers) and client optimistic-message timing; wrong idle gates could complete turns early or leave hangs, but behavior is heavily unit-tested and Grok-scoped.

Overview
Grok runs that already streamed a reply but left session/prompt hanging no longer stay on Working forever. The ACP v2 adapter gains debounced idle settlement (root assistant output quiescent, no tools/subagents/pending approvals), a 30s liveness watchdog for silent hangs, keepalive filtering, and re-arming of those timers when approval/user-input clears. Grok opts in via flavor flags; idle settle calls runtime.cancel on the wedged RPC.

xAI prompt_complete handling is reworked: makeXAiPromptCompletionRuntime races the prompt RPC against root-session _x.ai/session/prompt_complete (session-matched), injects promptId/requestId in _meta, and ignores foreign session ids.

Steer UX: optimistic user rows stop flashing away when message.updated beats turn-item.updatedChatView now treats committed ids from visible user_message turn items (deriveCommittedServerUserMessageIds) instead of all projection.messages.

Tests updated for settlement helpers and xAI extension; minor test imports move to vite-plus/test.

Reviewed by Cursor Bugbot for commit c64ce4c. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Harden Grok v2 root-turn settlement with idle detection and liveness watchdog

  • Adds idle-settle logic to AcpAdapterV2 that debounces root-turn finalization when no streams, tools, or subagents are active, with a configurable drain period to catch trailing chunks.
  • Adds a liveness watchdog that fails the turn with provider_error if no root-session output arrives within a configured window and no runtime request (elicitation/permission) is pending.
  • Rewrites makeXAiPromptCompletionRuntime in XAiAcpExtension.ts so Grok prompts race the underlying RPC against a _x.ai/session/prompt_complete notification fallback, guarded by sessionId to ignore foreign sessions.
  • Enables idle settlement and the liveness watchdog for the Grok flavor in GrokAdapterV2.ts with settleRootTurnWhenIdle=true and interruptPromptOnCancel=false.
  • Fixes optimistic eviction in ChatView to use committed user turn items instead of all projection messages, preventing premature dropping of steer rows.
  • Risk: Grok turns now auto-finalize on idle and fail if the liveness window expires, which changes behavior for long-running or slow-starting Grok responses.

Macroscope summarized c64ce4c.

@coderabbitai

coderabbitai Bot commented Jun 27, 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

Run ID: d7b0a763-3d1d-4518-b9b4-5b801e235eee

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
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@github-actions github-actions Bot added size:L 100-499 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list. labels Jun 27, 2026
@mwolson mwolson marked this pull request as ready for review June 27, 2026 00:43
hasRunningTool,
hasPendingRuntimeRequest,
hasActivePlan: context.plan !== null,
hasOutput: context.assistant.nextSegment > 0,

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.

🟠 High Adapters/AcpAdapterV2.ts:2163

acpRootTurnIsIdle requires hasOutput to be true before settling, but hasOutput is set to context.assistant.nextSegment > 0 — it only counts assistant text segments. When the turn produces tool calls or plan updates but no assistant text, acpRootTurnIsIdle always returns false, so the turn never settles. Simultaneously, the liveness watchdog bails out because acpRootTurnHasIngestedOutput(context) returns true for tool/plan output. If runtime.prompt also never returns, the turn is stuck in running forever — it neither settles nor fails.

Also found in 1 other location(s)

apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.ts:136

Enabling both settleRootTurnWhenIdle and rootTurnPromptLivenessMs for Grok leaves a turn stuck running when Grok emits only reasoning/tool/plan output and then strands session/prompt. The idle-settle path only completes turns after assistant text (hasOutput: context.assistant.nextSegment > 0), while the watchdog is disabled by any ingested output (acpRootTurnHasIngestedOutput treats reasoning/tools/plan as output). In that case neither path finalizes the turn, so the UI can stay on Working indefinitely.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts around line 2163:

`acpRootTurnIsIdle` requires `hasOutput` to be true before settling, but `hasOutput` is set to `context.assistant.nextSegment > 0` — it only counts assistant text segments. When the turn produces tool calls or plan updates but no assistant text, `acpRootTurnIsIdle` always returns false, so the turn never settles. Simultaneously, the liveness watchdog bails out because `acpRootTurnHasIngestedOutput(context)` returns true for tool/plan output. If `runtime.prompt` also never returns, the turn is stuck in `running` forever — it neither settles nor fails.

Also found in 1 other location(s):
- apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.ts:136 -- Enabling both `settleRootTurnWhenIdle` and `rootTurnPromptLivenessMs` for Grok leaves a turn stuck `running` when Grok emits only reasoning/tool/plan output and then strands `session/prompt`. The idle-settle path only completes turns after assistant text (`hasOutput: context.assistant.nextSegment > 0`), while the watchdog is disabled by any ingested output (`acpRootTurnHasIngestedOutput` treats reasoning/tools/plan as output). In that case neither path finalizes the turn, so the UI can stay on Working indefinitely.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Out of scope for this PR — idle settle intentionally requires assistant text to avoid early completion during tool-heavy turns. No repro in manual validation or grok replays. Listed under Out of scope in the PR description.

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.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

Comment thread apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts
Comment thread apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts
Comment thread apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts
Comment thread apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts Outdated
() => new Set(serverProjection?.messages.map((message) => message.id) ?? []),
[serverProjection],
() => deriveCommittedServerUserMessageIds(serverVisibleTurnItems),
[serverVisibleTurnItems],

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.

User-only count breaks env locking

Medium Severity

committedServerMessageIds now holds only visible user_message turn item ids, but activeMessageCount still uses that set’s size for envLocked and canOverrideServerThreadEnvMode. During the projection-vs-turn-item gap (and vs all projection.messages), the thread can look message-empty and allow env overrides that used to be blocked.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 61f4adb. Configure here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Out of scope — steer fix intentionally keys off visible user turn items. Brief projection gap only. Out of scope in PR description.

Comment thread apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts Outdated
@macroscopeapp

macroscopeapp Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

6 blocking correctness issues found. This PR introduces significant runtime behavior changes to Grok v2 orchestration including new settlement logic and liveness watchdogs. Multiple unresolved review comments identify potential bugs (tool-only turns never settling, interrupt handling issues) that warrant human attention.

You can customize Macroscope's approvability policy. Learn more.

Comment thread apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts
hasRunningTool,
hasPendingRuntimeRequest,
hasActivePlan: context.plan !== null,
hasOutput: context.assistant.nextSegment > 0,

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.

Tool-only turns never settle

Medium Severity

Idle settlement requires context.assistant.nextSegment > 0, while the 30s liveness watchdog skips failure once hasRootActivity or acpRootTurnHasIngestedOutput is true. A root turn that only ingests tools/reasoning with a stranded session/prompt can stay running forever with neither idle settle nor the watchdog firing.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 1e43aa0. Configure here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Out of scope follow-up — not required for the shipped repro. Out of scope in PR description.

driver: GROK_PROVIDER,
capabilities: GrokProviderCapabilitiesV2,
settleRootTurnWhenIdle: true,
interruptPromptOnCancel: false,

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.

🟠 High Adapters/GrokAdapterV2.ts:137

makeGrokAdapterV2 sets interruptPromptOnCancel: false, so cancelling a Grok turn calls session/cancel without first interrupting the local prompt fiber. The interruptPromptOnCancel option exists specifically to unblock Grok's wedged session/prompt calls, but leaving it false means a stuck prompt keeps the turn in running after Stop/cancel. Consider setting it to true.

Suggested change
interruptPromptOnCancel: false,
interruptPromptOnCancel: true,
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.ts around line 137:

`makeGrokAdapterV2` sets `interruptPromptOnCancel: false`, so cancelling a Grok turn calls `session/cancel` without first interrupting the local prompt fiber. The `interruptPromptOnCancel` option exists specifically to unblock Grok's wedged `session/prompt` calls, but leaving it `false` means a stuck prompt keeps the turn in `running` after Stop/cancel. Consider setting it to `true`.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Intentional: interruptPromptOnCancel stays false because true breaks message_steering/grok replay. PR description updated under Out of scope; idle settle uses runtime.cancel (session/cancel).

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.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

* `_x.ai/session/prompt_complete` notifications (sessionId-matched). Subagent
* completions on foreign session ids are ignored by the pending-entry gate.
*/
export const makeXAiPromptCompletionRuntime = Effect.fn("makeXAiPromptCompletionRuntime")(

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 acp/XAiAcpExtension.ts:455

The dedup guard in resolveXAiPromptCompletionFallback only checks completedPromptIdsRef when notification.promptId is defined. When a _x.ai/session/prompt_complete notification arrives without a promptId, the guard is skipped and the code resolves the first pending entry matching by sessionId alone. Because makeXAiPromptCompletionRuntime registers a pending entry for every new prompt before the RPC completes, a late duplicate notification from a previous turn (without promptId) will match the new prompt's pending entry and resolve it with the stale notification's stopReason and agentResult instead of waiting for the new prompt's own response. Consider tracking completed sessions alongside completed prompt IDs so that promptId-less notifications can be deduplicated per session.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/acp/XAiAcpExtension.ts around line 455:

The dedup guard in `resolveXAiPromptCompletionFallback` only checks `completedPromptIdsRef` when `notification.promptId` is defined. When a `_x.ai/session/prompt_complete` notification arrives without a `promptId`, the guard is skipped and the code resolves the first pending entry matching by `sessionId` alone. Because `makeXAiPromptCompletionRuntime` registers a pending entry for every new prompt before the RPC completes, a late duplicate notification from a previous turn (without `promptId`) will match the new prompt's pending entry and resolve it with the stale notification's `stopReason` and `agentResult` instead of waiting for the new prompt's own response. Consider tracking completed sessions alongside completed prompt IDs so that `promptId`-less notifications can be deduplicated per session.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Out of scope — serialized root prompts limit blast radius today. Out of scope in PR description.

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.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

Comment thread apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts
Comment thread apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.ts
Comment on lines +529 to +533
cancel: Effect.gen(function* () {
const started = yield* runtime.start();
yield* abortPendingPromptCompletions(pendingXAiPromptCompletionsRef, started.sessionId);
yield* runtime.cancel;
}),

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 acp/XAiAcpExtension.ts:529

The cancel method calls runtime.start() unconditionally, which creates a new ACP session even when the session was never started. If a caller cancels a run before any prompt has been sent, this spawns a brand-new Grok session just to immediately cancel it, producing unnecessary provider-side side effects from a pure stop request. Consider guarding the start() call so it only runs when there are already pending prompt completions (which implies the session was previously started).

-      cancel: Effect.gen(function* () {
-        const started = yield* runtime.start();
-        yield* abortPendingPromptCompletions(pendingXAiPromptCompletionsRef, started.sessionId);
-        yield* runtime.cancel;
-      }),
+      cancel: Effect.gen(function* () {
+        const pending = yield* Ref.get(pendingXAiPromptCompletionsRef);
+        if (pending.length > 0) {
+          const started = yield* runtime.start();
+          yield* abortPendingPromptCompletions(pendingXAiPromptCompletionsRef, started.sessionId);
+        }
+        yield* runtime.cancel;
+      }),
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/acp/XAiAcpExtension.ts around lines 529-533:

The `cancel` method calls `runtime.start()` unconditionally, which creates a new ACP session even when the session was never started. If a caller cancels a run before any prompt has been sent, this spawns a brand-new Grok session just to immediately cancel it, producing unnecessary provider-side side effects from a pure stop request. Consider guarding the `start()` call so it only runs when there are already pending prompt completions (which implies the session was previously started).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Out of scope — low risk on the Grok path we validated. Out of scope in PR description.

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.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

Base optimistic eviction on visible user turn items instead of
projection.messages so steer rows do not disappear between message.updated
and turn-item.updated events.
@mwolson mwolson force-pushed the fix/grok-v2 branch 2 times, most recently from 4c4d0dd to 8f6ec37 Compare June 27, 2026 01:18
Comment thread apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts
Comment thread apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts
Comment thread apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts
Comment thread apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts
Comment thread apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts
Comment thread apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts
Comment thread apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts
Comment thread apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts
) {
return;
}
yield* finalizeTurn(context, "completed", undefined, { drainTrailingChunks: true });

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.

Idle settle ignores user interrupt

Medium Severity

trySettleRootTurnWhenIdle always calls finalizeTurn with status completed. After the idle check, finalizeTurn can yield during trailing-chunk drain without re-reading context.interrupted, so a concurrent interruptTurn can still terminalize the run as completed instead of interrupted.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 9da925e. Configure here.

@github-actions github-actions Bot added size:XL 500-999 changed lines (additions + deletions). and removed size:L 100-499 changed lines (additions + deletions). labels Jun 27, 2026
),
);
}),
cancel: Effect.gen(function* () {

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 acp/XAiAcpExtension.ts:529

The cancel method calls abortPendingPromptCompletions for the entire session, which completes every pending fallback deferred with a synthetic cancelled response. Because prompt() registers its fallback deferred before the base runtime's serialization semaphore allows the RPC to execute, a prompt queued behind an active one is also in that list. When two callers invoke prompt() concurrently and the user cancels the active run, the second queued prompt receives cancelled even though it was never the active prompt and the base runtime would have allowed it to run afterward. Consider tracking the active prompt separately so cancel only aborts that one, leaving queued prompts intact.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/acp/XAiAcpExtension.ts around line 529:

The `cancel` method calls `abortPendingPromptCompletions` for the entire session, which completes every pending fallback deferred with a synthetic `cancelled` response. Because `prompt()` registers its fallback deferred before the base runtime's serialization semaphore allows the RPC to execute, a prompt queued behind an active one is also in that list. When two callers invoke `prompt()` concurrently and the user cancels the active run, the second queued prompt receives `cancelled` even though it was never the active prompt and the base runtime would have allowed it to run afterward. Consider tracking the active prompt separately so `cancel` only aborts that one, leaving queued prompts intact.

@mwolson mwolson force-pushed the fix/grok-v2 branch 2 times, most recently from 46fc8ca to 938e815 Compare June 27, 2026 02:35
requestId: fallback.promptId,
},
})
.pipe(Effect.forkChild);

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 acp/XAiAcpExtension.ts:504

Effect.forkChild attaches promptRpcFiber to the caller's scope, so when the outer effect is interrupted (timeout, scope shutdown, parent cancellation) the scope auto-interrupts the fiber. The catchCause on Fiber.join then converts that interrupt-only cause into a synthetic { stopReason: "cancelled" } success, swallowing the real caller interruption and making aborted runs look like clean provider cancellations. The ensuring block already calls Fiber.interrupt(promptRpcFiber) for cleanup, so using Effect.forkDetach instead would prevent the scope from auto-interrupting the fiber and let the interruption propagate normally.

-            .pipe(Effect.forkChild);
+            .pipe(Effect.forkDetach);
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/acp/XAiAcpExtension.ts around line 504:

`Effect.forkChild` attaches `promptRpcFiber` to the caller's scope, so when the outer effect is interrupted (timeout, scope shutdown, parent cancellation) the scope auto-interrupts the fiber. The `catchCause` on `Fiber.join` then converts that interrupt-only cause into a synthetic `{ stopReason: "cancelled" }` success, swallowing the real caller interruption and making aborted runs look like clean provider cancellations. The `ensuring` block already calls `Fiber.interrupt(promptRpcFiber)` for cleanup, so using `Effect.forkDetach` instead would prevent the scope from auto-interrupting the fiber and let the interruption propagate normally.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Deferring the forkDetach change for now. The post-completion Interrupt in provider logs on manual retest is intentional: idle settle calls finalizeTurn first, then runtime.cancel to tear down the hung session/prompt RPC. The prompt fork already no-ops when context.finalized is true, so the synthetic cancelled response from catchCause does not affect turn status.

Scope interruption swallowing is a separate hardening concern; added to the PR out-of-scope table for a follow-up pass if we get a repro.

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.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

@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.

There are 4 total unresolved issues (including 3 from previous reviews).

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 938e815. Configure here.

Comment thread apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts Outdated
Settle root Grok turns from idle root-session output when session/prompt
stays hung, with debounced completion and a liveness watchdog for silent
post-prompt hangs. Restore root-matched prompt_complete racing in the xAI
extension without treating subagent completions as root terminalization.

Fix replay/CI regressions: use forkIn(sessionScope) instead of the removed
forkDaemon API, drop drainEvents (orchestration projects updates via
handleSessionUpdate, not getEvents), and avoid TestClock-blocking sleeps in
the completion drain path. Keep interruptPromptOnCancel off so steering replay
can cancel in-flight partial assistant output before restart.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL 500-999 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