Skip to content

Add opt-in recovery for exact-empty CUA responses#55

Merged
rgarcia merged 5 commits into
mainfrom
hypeship/retry-empty-responses-2-fix
Jul 10, 2026
Merged

Add opt-in recovery for exact-empty CUA responses#55
rgarcia merged 5 commits into
mainfrom
hypeship/retry-empty-responses-2-fix

Conversation

@rgarcia

@rgarcia rgarcia commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Summary

  • preserve pi's default behavior: a successful exact-empty assistant response completes the run when recovery is not configured
  • add explicit, bounded empty-response recovery for CuaAgent and CuaAgentHarness with caller-supplied follow-up text
  • use pi's public followUp() queue so queued caller work takes precedence and recovery remains a distinct billed model turn
  • validate recovery configuration and document completion, cost, and history semantics
  • cover recovery budgets, queue modes, cancellation, content and stop-reason boundaries, tool use, harness nextTurn behavior, and queue-state reset between prompts

Testing

  • npm run typecheck
  • npm test --workspace @onkernel/cua-agent -- --exclude '**/*.live.test.ts' (160 passed)
  • npm test --workspace @onkernel/cua-cli (82 passed, 5 skipped)
  • git diff --check

Note

Medium Risk
Changes agent turn completion and can trigger extra billed provider calls when enabled; default-off and queue/abort guards limit blast radius.

Overview
Adds optional empty response recovery on CuaAgent and CuaAgentHarness via emptyResponseRecovery: { followUp, maxAttempts }. When omitted or maxAttempts: 0, behavior stays the same as pi: an assistant turn with empty content and stopReason: "stop" ends the run.

With recovery enabled, a turn_end matching that exact-empty pattern queues a configured user message through pi's followUp(), keeps the empty assistant message in history, and runs another provider turn until maxAttempts is hit. Recovery resets each top-level prompt; it skips when the run is aborted, caller queues already have steer/follow-up work, or the response is not exact-empty (whitespace/thinking blocks, non-stop reasons, tool-use turns).

Constructor validation rejects blank followUp and invalid maxAttempts. README documents cost, defaults, and history semantics. CuaEmptyResponseRecoveryOptions is exported; tests cover both agent and harness paths.

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

@rgarcia rgarcia changed the title Retry exact-empty CUA responses once Add opt-in recovery for exact-empty CUA responses Jul 9, 2026
@rgarcia rgarcia marked this pull request as ready for review July 9, 2026 21:57

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

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

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Stale harness queue blocks recovery
    • CuaAgentHarness now clears its cached pending-queue flag at each before_agent_start so stale queue snapshots cannot suppress empty-response recovery in later runs.

Create PR

Or push these changes by commenting:

@cursor push 557a29fbb0
Preview (557a29fbb0)
diff --git a/packages/agent/src/agent.ts b/packages/agent/src/agent.ts
--- a/packages/agent/src/agent.ts
+++ b/packages/agent/src/agent.ts
@@ -533,6 +533,7 @@
 		if (recovery && recovery.maxAttempts > 0) {
 			this.on("before_agent_start", () => {
 				this.emptyResponseRecoveryAttempts = 0;
+				this.hasPendingActiveQueue = false;
 				return undefined;
 			});
 			this.subscribe(async (event, signal) => {

diff --git a/packages/agent/test/agent.test.ts b/packages/agent/test/agent.test.ts
--- a/packages/agent/test/agent.test.ts
+++ b/packages/agent/test/agent.test.ts
@@ -767,6 +767,24 @@
 		expect(scripted.calls()).toBe(4);
 	});
 
+	it("clears stale queue snapshots before a new prompt starts", async () => {
+		const scripted = createScriptedModels([undefined, "finished"]);
+		const harness = new CuaAgentHarness({
+			...(await createHarnessServices()),
+			browser,
+			client,
+			model: "openai:gpt-5.5",
+			models: scripted.models,
+			emptyResponseRecovery: recovery,
+		});
+
+		(harness as unknown as { hasPendingActiveQueue: boolean }).hasPendingActiveQueue = true;
+		const response = await harness.prompt("finish the task");
+
+		expect(scripted.calls()).toBe(2);
+		expect(response.content).toEqual([{ type: "text", text: "finished" }]);
+	});
+
 	it("makes exactly one additional call per configured attempt", async () => {
 		const scripted = createScriptedModels([undefined, undefined, undefined]);
 		const harness = new CuaAgentHarness({

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit 08fdeff. Configure here.

Comment thread packages/agent/src/agent.ts
@rgarcia rgarcia merged commit e53c87a into main Jul 10, 2026
6 checks passed
@rgarcia rgarcia deleted the hypeship/retry-empty-responses-2-fix branch July 10, 2026 16:28
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