From be3b21a974a263b4a592276c2d732fb6bbd92e7b Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Thu, 23 Apr 2026 09:39:26 -0700 Subject: [PATCH] base-action: persist execution log when SDK throws MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the Agent SDK's query() iterator throws — most notably on max-turns exhaustion — the existing catch block re-throws before reaching the writeFile/setOutput code below, so: - $RUNNER_TEMP/claude-execution-output.json is never written - the execution_file / session_id step outputs are never set - any downstream step gated on steps.claude.outputs.execution_file (e.g. an S3 upload of the transcript) silently skips, and the partial transcript is lost Fix by capturing the SDK error, writing the transcript and setting the outputs unconditionally, then re-throwing so the step still fails. Outputs are now published from within this function because index.ts re-throws on failure before reaching its own setOutput calls. Repro: a job that hits --max-turns 1000 exits with exit code 1, no execution_file output, no log persisted. Co-Authored-By: Claude Opus 4.7 (1M context) --- base-action/src/run-claude-sdk.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/base-action/src/run-claude-sdk.ts b/base-action/src/run-claude-sdk.ts index e37184a7f..4ccfa7b48 100644 --- a/base-action/src/run-claude-sdk.ts +++ b/base-action/src/run-claude-sdk.ts @@ -156,6 +156,7 @@ export async function runClaudeWithSdk( const messages: SDKMessage[] = []; let resultMessage: SDKResultMessage | undefined; + let sdkError: unknown; try { for await (const message of query({ prompt, options: sdkOptions })) { @@ -172,18 +173,24 @@ export async function runClaudeWithSdk( } } catch (error) { console.error("SDK execution error:", error); - throw new Error(`SDK execution error: ${error}`); + sdkError = error; } const result: ClaudeRunResult = { conclusion: "failure", }; - // Write execution file + // Persist the execution file regardless of whether the SDK threw, so + // failures like max-turns still leave a partial transcript on disk for + // downstream upload/inspection steps. try { await writeFile(EXECUTION_FILE, JSON.stringify(messages, null, 2)); console.log(`Log saved to ${EXECUTION_FILE}`); result.executionFile = EXECUTION_FILE; + // Publish the output eagerly: index.ts re-throws on SDK errors before + // reaching its own setOutput calls, so steps gated on `execution_file` + // would otherwise never see it on the failure path. + core.setOutput("execution_file", EXECUTION_FILE); } catch (error) { core.warning(`Failed to write execution file: ${error}`); } @@ -195,6 +202,11 @@ export async function runClaudeWithSdk( if (initMessage && "session_id" in initMessage && initMessage.session_id) { result.sessionId = initMessage.session_id as string; core.info(`Set session_id: ${result.sessionId}`); + core.setOutput("session_id", result.sessionId); + } + + if (sdkError) { + throw new Error(`SDK execution error: ${sdkError}`); } if (!resultMessage) {