Skip to content

fix(desktop,runtime): interrupt and bound empty assistant loops - #4138

Open
Rangsh wants to merge 3 commits into
apache:mainfrom
Rangsh:fix/empty-ai-reply-loop-interrupt
Open

fix(desktop,runtime): interrupt and bound empty assistant loops#4138
Rangsh wants to merge 3 commits into
apache:mainfrom
Rangsh:fix/empty-ai-reply-loop-interrupt

Conversation

@Rangsh

@Rangsh Rangsh commented Aug 29, 2026

Copy link
Copy Markdown

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 maxSteps is 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 — pass
  • Desktop follow-up submit routing tests — pass
  • Rebased onto current apache/maka main and resolved conflict with fix(runtime): preserve Plan final responses #3886 in ai-sdk-backend.test.ts
  • Did not run full-repo lint / format:check / typecheck / full workspace npm test

AI use

Select exactly one:

  • No generative tool made a substantive contribution
  • Generative tooling made a substantive contribution

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: Cursor

Checklist

  • Tests cover the change and fail without it
  • Lint, format, typecheck and the affected suites pass locally

Does this PR entail a change in behavior?

  • Yes — described under Summary above
  • No

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 Astro-Han 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.

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.

Comment thread packages/runtime/src/ai-sdk-backend.ts Outdated
stepTextPartStartOffset,
);
} else if (event.kind === 'thinking') {
stepSawThinking = 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.

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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

Rangsh commented Aug 30, 2026

Copy link
Copy Markdown
Author

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.

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

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

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.

Comment thread packages/runtime/src/ai-sdk-backend.ts Outdated
text: event.text,
} satisfies ThinkingDeltaEvent);
} else if (event.kind === 'thinking-signature') {
stepSawThinking = 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.

[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>
@Rangsh

Rangsh commented Aug 31, 2026

Copy link
Copy Markdown
Author

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.

Agreed — a standalone signature is omitted/redacted reasoning, not user-visible progress.

thinking-signature still persists on stepSignature for replay, but it no longer sets stepSawThinking. The empty-step cap now treats signature-only + identical tool calls as textless.

Added a regression: Anthropic signature-only delta (no thinking text) + the same tool-call repeats, then the turn ends at step_limit after 3 steps, and the signature is still on thinking_complete.

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

Labels

effort/M Under 500 readable lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: maka loop leads to repeated empty AI replies and cannot be interrupted by message

2 participants