From 79f417e9c337f9b347ef0b89e2549f935f618827 Mon Sep 17 00:00:00 2001 From: Khaliq Date: Tue, 10 Mar 2026 11:12:47 +0100 Subject: [PATCH 1/3] fix: treat force-released agent as step failure, not success When an agent is force-released after idle nudging, the runner was returning a stub success string ("Agent completed (force-released after idle nudging)") which poisoned downstream steps that depend on {{steps.X.output}}. Now throw an error instead so the retry loop is triggered. Closes #498 Co-Authored-By: Claude Opus 4.6 --- packages/sdk/src/workflows/runner.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/sdk/src/workflows/runner.ts b/packages/sdk/src/workflows/runner.ts index 9ca75c6fa..1d6e978a0 100644 --- a/packages/sdk/src/workflows/runner.ts +++ b/packages/sdk/src/workflows/runner.ts @@ -3448,6 +3448,12 @@ export class WorkflowRunner { throw new Error(`Step "${step.name}" timed out after ${timeoutMs ?? 'unknown'}ms`); } } + + if (exitResult === 'released') { + throw new Error( + `Step "${step.name}" failed — agent was force-released after idle timeout without completing` + ); + } } finally { // Snapshot PTY chunks before cleanup — we need them for output reading below ptyChunks = this.ptyOutputBuffers.get(agentName) ?? []; @@ -3476,9 +3482,7 @@ export class WorkflowRunner { ? await readFile(summaryPath, 'utf-8') : exitResult === 'timeout' ? 'Agent completed (released after idle timeout)' - : exitResult === 'released' - ? 'Agent completed (force-released after idle nudging)' - : `Agent exited (${exitResult})`; + : `Agent exited (${exitResult})`; } return output; From b9c94032a05fda89588414c1c546c1f4f147c896 Mon Sep 17 00:00:00 2001 From: Khaliq Date: Tue, 10 Mar 2026 11:57:32 +0100 Subject: [PATCH 2/3] fix: distinguish force-released (nudge exhaustion) from released (idle-complete) The previous fix threw on all `exitResult === 'released'`, which breaks the no-nudge-config path where idle means intentional success. Changes: - waitForExitWithIdleNudging returns 'force-released' (not 'released') when nudges are exhausted and agent is force-released - spawnAndWait only throws on 'force-released', preserving 'released' as the idle-complete success path - Fallback output ternary restored for 'released' (idle success) - Error message updated: "exhausting idle nudges" instead of "idle timeout" Co-Authored-By: Claude Opus 4.6 --- packages/sdk/src/workflows/runner.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/sdk/src/workflows/runner.ts b/packages/sdk/src/workflows/runner.ts index 1d6e978a0..23472dc17 100644 --- a/packages/sdk/src/workflows/runner.ts +++ b/packages/sdk/src/workflows/runner.ts @@ -3449,9 +3449,9 @@ export class WorkflowRunner { } } - if (exitResult === 'released') { + if (exitResult === 'force-released') { throw new Error( - `Step "${step.name}" failed — agent was force-released after idle timeout without completing` + `Step "${step.name}" failed — agent was force-released after exhausting idle nudges without completing` ); } } finally { @@ -3482,7 +3482,9 @@ export class WorkflowRunner { ? await readFile(summaryPath, 'utf-8') : exitResult === 'timeout' ? 'Agent completed (released after idle timeout)' - : `Agent exited (${exitResult})`; + : exitResult === 'released' + ? 'Agent completed (idle — treated as done)' + : `Agent exited (${exitResult})`; } return output; @@ -3521,7 +3523,7 @@ export class WorkflowRunner { agentDef: AgentDefinition, step: WorkflowStep, timeoutMs?: number - ): Promise<'exited' | 'timeout' | 'released'> { + ): Promise<'exited' | 'timeout' | 'released' | 'force-released'> { const nudgeConfig = this.currentConfig?.swarm.idleNudge; if (!nudgeConfig) { // Idle = done: race exit against idle. Whichever fires first completes the step. @@ -3591,7 +3593,7 @@ export class WorkflowRunner { ); this.emit({ type: 'step:force-released', runId: this.currentRunId ?? '', stepName: step.name }); await agent.release(); - return 'released'; + return 'force-released'; } } From f458d782484b7fcd7ba3332656e739b9e8fd2b05 Mon Sep 17 00:00:00 2001 From: Khaliq Date: Tue, 10 Mar 2026 13:43:03 +0100 Subject: [PATCH 3/3] fix: use timeoutMs directly in nudge loop timeout guard Co-Authored-By: Claude Opus 4.6 --- packages/sdk/src/workflows/runner.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sdk/src/workflows/runner.ts b/packages/sdk/src/workflows/runner.ts index 23472dc17..3b972532c 100644 --- a/packages/sdk/src/workflows/runner.ts +++ b/packages/sdk/src/workflows/runner.ts @@ -3574,7 +3574,7 @@ export class WorkflowRunner { } // Agent is still running after the window expired. - if (remaining !== undefined && Date.now() - startTime >= remaining) { + if (timeoutMs !== undefined && Date.now() - startTime >= timeoutMs) { return 'timeout'; }