From e7ffb1202787a9594837bc90e987fda683da24f8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 20 Sep 2026 08:02:19 +0000 Subject: [PATCH 1/2] Surface Pi provider errors that end a turn with empty content MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit transformPiV3Entries/computePiV3Stats only rendered turn_end message content (text/toolCall); a turn that fails at the provider request level (e.g. HTTP 400 model_not_supported) ends with content: [] and no tool calls, so the only trace of the failure — the top-level errorMessage — was silently dropped, producing a step summary with no visible explanation for why the run had no output. Found while auditing run 35497537350 (AI Moderator workflow), whose Pi CLI turn failed with a 400 model_not_supported error and rendered an empty conversation. Co-Authored-By: Claude Sonnet 5 --- actions/setup/js/parse_pi_log.cjs | 15 ++++++++++- actions/setup/js/parse_pi_log.test.cjs | 35 ++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/actions/setup/js/parse_pi_log.cjs b/actions/setup/js/parse_pi_log.cjs index 80f91396a83..83dcf52e205 100644 --- a/actions/setup/js/parse_pi_log.cjs +++ b/actions/setup/js/parse_pi_log.cjs @@ -87,6 +87,7 @@ function parsePiLog(logContent) { }, duration_ms: stats.duration_ms || 0, num_turns: stats.turns || 0, + errors: stats.errors && stats.errors.length > 0 ? stats.errors : undefined, }; markdown += generateInformationSection(syntheticEntry); @@ -96,6 +97,7 @@ function parsePiLog(logContent) { type: "result", num_turns: syntheticEntry.num_turns, usage: syntheticEntry.usage, + errors: syntheticEntry.errors, }); } else { markdown += generateInformationSection(null); @@ -365,13 +367,15 @@ function isPiV3ResultError(result) { * finalized turns. * * @param {Array} rawEntries - Raw parsed JSONL entries - * @returns {{input_tokens:number, output_tokens:number, turns:number, duration_ms:number}|null} Stats or null when unavailable + * @returns {{input_tokens:number, output_tokens:number, turns:number, duration_ms:number, errors:Array}|null} Stats or null when unavailable */ function computePiV3Stats(rawEntries) { let outputTokens = 0; let inputTokens = 0; let turns = 0; let sawUsage = false; + /** @type {Array} */ + const errors = []; for (const raw of rawEntries) { if (raw.type !== "turn_end") { @@ -388,6 +392,14 @@ function computePiV3Stats(rawEntries) { inputTokens += usage.input; } } + // A turn can end with an empty content array and no toolCalls when the provider + // request itself failed (e.g. a 400 model_not_supported response); the only trace + // of the failure is this top-level errorMessage, which would otherwise render as a + // silent, content-free turn in the step summary. + const errorMessage = raw.message && typeof raw.message.errorMessage === "string" ? raw.message.errorMessage : undefined; + if (errorMessage) { + errors.push(errorMessage); + } } if (turns === 0 && !sawUsage) { @@ -399,6 +411,7 @@ function computePiV3Stats(rawEntries) { output_tokens: outputTokens, turns: turns, duration_ms: 0, + errors: errors, }; } diff --git a/actions/setup/js/parse_pi_log.test.cjs b/actions/setup/js/parse_pi_log.test.cjs index bf3ec24631a..bc7708d1456 100644 --- a/actions/setup/js/parse_pi_log.test.cjs +++ b/actions/setup/js/parse_pi_log.test.cjs @@ -304,6 +304,41 @@ describe("parse_pi_log.cjs", () => { expect(toolResult.message.content[0].is_error).toBe(true); expect(toolResult.message.content[0].content).toContain("boom"); }); + + it("surfaces a provider-level errorMessage from an empty-content turn_end (e.g. model_not_supported)", () => { + // Reproduces a real failed run: the provider request itself fails (HTTP 400), so + // turn_end carries content: [] with no assistant text or tool calls — the only trace + // of the failure is the top-level errorMessage on the turn_end message. + const failedTurnLines = [ + { type: "session", version: 3, id: "s1" }, + { type: "agent_start" }, + { type: "turn_start" }, + { + type: "turn_end", + message: { + role: "assistant", + model: "auto", + content: [], + stopReason: "error", + usage: { input: 0, output: 0 }, + errorMessage: '400: {"message":"The requested model is not supported.","code":"model_not_supported","param":"model","type":"invalid_request_error"}', + }, + toolResults: [], + }, + { type: "agent_end", messages: [], willRetry: false }, + { type: "agent_settled" }, + ]; + const failedLog = failedTurnLines.map(l => JSON.stringify(l)).join("\n"); + + const stats = computePiV3Stats(failedTurnLines); + expect(stats.errors).toEqual([expect.stringContaining("model_not_supported")]); + + const result = parsePiLog(failedLog); + expect(result.markdown).toContain("model_not_supported"); + + const resultEntry = result.logEntries.find(e => e.type === "result"); + expect(resultEntry.errors).toEqual([expect.stringContaining("model_not_supported")]); + }); }); describe("normalizePiToolName", () => { From 5b1a17d34fd864384e017836ed4d265a7a4da79b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 21 Sep 2026 15:12:06 +0000 Subject: [PATCH 2/2] Fix Pi parser stats typecheck Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- actions/setup/js/parse_pi_log.cjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/actions/setup/js/parse_pi_log.cjs b/actions/setup/js/parse_pi_log.cjs index 83dcf52e205..8641c7000b2 100644 --- a/actions/setup/js/parse_pi_log.cjs +++ b/actions/setup/js/parse_pi_log.cjs @@ -418,7 +418,7 @@ function computePiV3Stats(rawEntries) { /** * Extracts stats from a legacy Pi `result` event, preserving the original flat-schema behavior. * @param {Array} rawEntries - Raw parsed JSONL entries - * @returns {{input_tokens:number, output_tokens:number, turns:number, duration_ms:number}|null} Stats or null when absent + * @returns {{input_tokens:number, output_tokens:number, turns:number, duration_ms:number, errors:Array}|null} Stats or null when absent */ function legacyPiStats(rawEntries) { const resultEntry = rawEntries.find(e => e.type === "result"); @@ -431,6 +431,7 @@ function legacyPiStats(rawEntries) { output_tokens: stats.output_tokens || 0, turns: stats.turns || 0, duration_ms: stats.duration_ms || 0, + errors: [], }; }