fix: break SDK iterator after result message to prevent hang (#1339)

In some workflow contexts — reliably reproducible for us on
pull_request-triggered runs of this action — the Claude Agent SDK
query() async iterator does not close after the terminal result
message is emitted. The for-await loop in runClaudeWithSdk blocks
indefinitely after Claude has finished its work, until the workflow's
timeout-minutes cap kills the job.

Symptoms observed in production (4× in our scan-reviewer workflow):
- Claude completes successfully: SDK emits { type: "result",
  subtype: "success", ... } with the cost / turns / duration set.
- The action then sits with zero log output for the rest of
  timeout-minutes (we measured 18-19 min of dead time after result).
- The job is cancelled at timeout. writeExecutionFile is never
  called → no claude-execution-output.json → cost-tracker and other
  post-steps see nothing.
- Run shows as cancelled, even though Claude did its work and any
  verdict it posted via gh tools already landed.

Author-mode (workflow_dispatch) runs from the same codebase
terminate cleanly the same day, so the hang is specific to certain
event triggers.

By SDK contract the result message is terminal — no further messages
follow. Break out of the loop immediately after capturing it,
regardless of whether the upstream iterator ever closes. If the SDK
is later fixed to close cleanly in all contexts, this break becomes
a no-op.
This commit is contained in:
Stephen Cobbe 2026-06-11 21:17:47 -07:00 committed by GitHub
parent cd59d5df0d
commit 84d317e8f9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -167,6 +167,16 @@ export async function runClaudeWithSdk(
if (message.type === "result") {
resultMessage = message as SDKResultMessage;
// The SDK's query() iterator should close itself after the
// result message, but in some workflow contexts (notably
// pull_request-triggered runs) it stays open indefinitely and
// the for-await hangs until the workflow's timeout-minutes
// kills the job. This causes the action to "succeed" inside
// Claude (verdict posted, $cost recorded) but be reported as
// cancelled with no execution-output.json written. Break
// explicitly: by SDK contract no further messages follow a
// result, so the break is safe.
break;
}
}
} catch (error) {