fix(desktop,runtime): interrupt and bound empty assistant loops - #4138
fix(desktop,runtime): interrupt and bound empty assistant loops#4138Rangsh wants to merge 3 commits into
Conversation
Interrupt the active desktop turn before sending a plain Enter follow-up, and clean up transient messages retracted by the stop operation. Bound repeated textless tool steps when no explicit maxSteps is configured, while preserving normal multi-step workflows and explicit step limits. Fixes apache#4083
Astro-Han
left a comment
There was a problem hiding this comment.
Thanks for addressing the output-free tool loop in the shared Runtime boundary. The cap is a useful safety net and keeps explicit maxSteps authoritative. I found one normal Responses-protocol path that still bypasses the new counter.
This is a suggestion from an outside review, so please push back if the empty reasoning carrier has a different production invariant than the adapter currently expresses.
中文摘要
感谢把无输出 tool loop 的保护放在共享 Runtime 边界。当前仍有 1 个 P1:OpenAI Responses 的空 reasoning carrier 会把 stepSawThinking 置真,使完全无可见进展的重复 tool step 永远不进入新计数器。若我遗漏了 provider invariant,欢迎直接 push back。
AI-assisted review disclosure: Codex coordinated an independent reviewer lane; Astro-Han independently checked the exact head, adapter composition, reachability, and severity, and owns this review.
| stepTextPartStartOffset, | ||
| ); | ||
| } else if (event.kind === 'thinking') { | ||
| stepSawThinking = true; |
There was a problem hiding this comment.
Thanks for adding the shared loop cap. I think this unconditional flag leaves a [P1] category ① production bypass for OpenAI Responses: the model adapter emits a thinking event with text: "" at reasoning-end whenever Responses provider metadata is present. A normal textless tool step can therefore set stepSawThinking=true even though it has no visible reasoning or text, so emptyStepSignature stays undefined and identical tool-only steps never reach the new cap. The added test uses a mock with no such carrier, so it does not exercise the real Responses composition. My suggestion is to count only non-empty visible thinking here (while preserving provider metadata separately) and add a Responses reasoning-end → repeated tool-call regression. Please push back if an empty carrier is intentionally considered user-visible progress.
There was a problem hiding this comment.
Thanks — that was a real bypass. An empty Responses reasoning-end carrier is not user-visible progress.
stepSawThinking now only flips on non-empty thinking text. Provider metadata still persists separately via sawStepThinking, so the encrypted carrier still round-trips.
Added a regression that streams Responses reasoning-end with empty text + identical tool-calls and asserts the loop cap still fires (step_limit after 3 steps).
OpenAI Responses emits an empty thinking carrier at reasoning-end whenever provider metadata is present. That is not user-visible progress, so it must not reset the identical textless tool-step counter (apache#4083). Generated-by: Cursor Co-authored-by: Cursor <cursoragent@cursor.com>
Thanks — that was a real bypass. An empty Responses
Added a regression that streams Responses |
Astro-Han
left a comment
There was a problem hiding this comment.
Thanks for bounding repeated empty assistant steps and for adding the Responses empty-reasoning regression. One normal provider carrier still bypasses the new cap. This is a suggestion from an outside review, so please do push back if a signature-only event is intentionally treated as user-visible progress.
AI-assisted review disclosure: Codex ran an independent runtime/retry analysis lane; Astro-Han is the contributor of record for this review.
| text: event.text, | ||
| } satisfies ThinkingDeltaEvent); | ||
| } else if (event.kind === 'thinking-signature') { | ||
| stepSawThinking = true; |
There was a problem hiding this comment.
[P1] (category ① — normal provider stream path)
Thanks for preserving the signature for continuation/replay. Setting stepSawThinking for a signature-only carrier also prevents emptyStepSignature from being computed. Anthropic/Responses can emit omitted or redacted reasoning as a standalone signature with no text (the adapter and current tests explicitly preserve that path), so a model that repeats the same textless tool call plus signature can still loop without ever reaching the three-step cap. Could the signature remain persisted without counting as visible progress, and add a signature-only + repeated-identical-tool-call regression? Please feel free to push back if the provider contract guarantees every such signature corresponds to substantive progress that should reset the bound.
A standalone Anthropic/Responses thinking signature is omitted or redacted reasoning, not user-visible progress. Persist it for replay, but do not let it prevent emptyStepSignature from bounding identical textless tool steps (apache#4083). Generated-by: Cursor Co-authored-by: Cursor <cursoragent@cursor.com>
Agreed — a standalone signature is omitted/redacted reasoning, not user-visible progress.
Added a regression: Anthropic signature-only delta (no thinking text) + the same tool-call repeats, then the turn ends at |
Interrupt the active desktop turn before sending a plain Enter follow-up, and clean up transient messages retracted by the stop operation.
Bound repeated textless tool steps when no explicit maxSteps is configured, while preserving normal multi-step workflows and explicit step limits.
Fixes #4083
Summary
Desktop mid-turn plain Enter used to queue a follow-up, so a runaway empty-reply loop never stopped and the typed message could not take effect. Plain Enter now interrupts the active turn first, then starts a new root send; Shift+Enter still steers. In the runtime agent loop, when
maxStepsis unset, consecutive identical textless tool-only steps are capped so the turn ends instead of flooding blank assistant replies, without changing normal multi-step work or explicit step budgets.Fixes #4083
Verification
node --test --test-name-pattern="stops an unbounded loop after consecutive identical empty" packages/runtime/dist/__tests__/ai-sdk-backend.test.js— passapache/makamainand resolved conflict with fix(runtime): preserve Plan final responses #3886 inai-sdk-backend.test.tslint/format:check/typecheck/ full workspacenpm testAI use
Select exactly one:
Tool(s) and scope:
Cursor (Composer) helped locate the empty-loop / mid-turn send paths, implement the runtime empty-step bound and Desktop interrupt-on-send behavior, add/adjust tests, and rebase through the upstream conflict. Human owns the final review and submission.
Affected commits should retain:
Generated-by: CursorChecklist
Does this PR entail a change in behavior?