From ffe69774ecbf7b021865182ccc4d25bb3954a142 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 21 Aug 2026 18:33:23 +0000 Subject: [PATCH 1/2] Render WSRF in token usage step summary Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/parse_token_usage.cjs | 46 ++++++++++++++++++--- actions/setup/js/parse_token_usage.test.cjs | 34 +++++++++++++++ 2 files changed, 75 insertions(+), 5 deletions(-) diff --git a/actions/setup/js/parse_token_usage.cjs b/actions/setup/js/parse_token_usage.cjs index 72d73ed6165..86966257e82 100644 --- a/actions/setup/js/parse_token_usage.cjs +++ b/actions/setup/js/parse_token_usage.cjs @@ -5,6 +5,7 @@ const fs = require("fs"); const { getErrorMessage } = require("./error_helpers.cjs"); const { ERR_PARSE } = require("./error_codes.cjs"); const { parseTokenUsageJsonl, generateTokenUsageSummary } = require("./parse_mcp_gateway_log.cjs"); +const { calculateWorkingSetFromJSONL } = require("./generate_usage_activity_summary.cjs"); /** * Parses the firewall proxy token-usage.jsonl and appends a collapsible markdown @@ -99,10 +100,42 @@ function getSummaryTitle() { * Builds the token usage section for the GitHub step summary. * @param {string} title * @param {string} markdown + * @param {ReturnType["workingSet"] | null} workingSet * @returns {string} */ -function buildStepSummarySection(title, markdown) { - return `
\n${title}\n\nPer-request AI credits and token totals\n\n${markdown}
\n\n`; +function buildStepSummarySection(title, markdown, workingSet = null) { + const workingSetSection = buildWorkingSetDetailsSection(workingSet); + return `
\n${title}\n\nPer-request AI credits and token totals\n\n${workingSetSection}${markdown}
\n\n`; +} + +/** + * Builds a progressive-disclosure block for the Working-Set Rebuild Factor. + * @param {ReturnType["workingSet"] | null} workingSet + * @returns {string} + */ +function buildWorkingSetDetailsSection(workingSet) { + if (!workingSet || typeof workingSet !== "object") return ""; + const measurementState = workingSet.measurement_state || "unavailable"; + const hasFactor = typeof workingSet.rebuild_factor === "number" && Number.isFinite(workingSet.rebuild_factor); + const displayFactor = hasFactor ? `${workingSet.rebuild_factor.toFixed(2)}×` : "unavailable"; + const displayInvocations = Number.isFinite(workingSet.invocations) ? workingSet.invocations.toLocaleString() : "0"; + const displayCumulative = Number.isFinite(workingSet.cumulative_input_tokens) ? workingSet.cumulative_input_tokens.toLocaleString() : "0"; + const displayPeak = Number.isFinite(workingSet.peak_input_tokens) ? workingSet.peak_input_tokens.toLocaleString() : "0"; + const displayExcess = Number.isFinite(workingSet.rebuild_excess_tokens) ? workingSet.rebuild_excess_tokens.toLocaleString() : "0"; + + return [ + "
", + `Working-Set Rebuild Factor (WSRF): ${displayFactor} (${measurementState})`, + "", + `- State: \`${measurementState}\``, + `- Invocations: ${displayInvocations}`, + `- Cumulative input tokens: ${displayCumulative}`, + `- Peak invocation input tokens: ${displayPeak}`, + `- Rebuild excess tokens: ${displayExcess}`, + "", + "
", + "", + ].join("\n"); } /** @@ -130,10 +163,11 @@ function renderTokenTableAsPlainText(title, markdown) { * Falls back to the Actions summary API when the summary path is unavailable. * @param {string} title * @param {string} markdown + * @param {ReturnType["workingSet"] | null} workingSet * @returns {Promise} */ -async function appendStepSummarySection(title, markdown) { - const section = buildStepSummarySection(title, markdown); +async function appendStepSummarySection(title, markdown, workingSet = null) { + const section = buildStepSummarySection(title, markdown, workingSet); const summaryPath = process.env.GITHUB_STEP_SUMMARY; if (summaryPath) { try { @@ -168,9 +202,10 @@ async function main() { return; } const markdown = generateTokenUsageSummary(summary); + const workingSet = calculateWorkingSetFromJSONL(content).workingSet; if (markdown.length > 0) { core.info(renderTokenTableAsPlainText(getSummaryTitle(), markdown)); - await appendStepSummarySection(getSummaryTitle(), markdown); + await appendStepSummarySection(getSummaryTitle(), markdown, workingSet); } core.info("Token usage summary appended to step summary"); @@ -231,6 +266,7 @@ if (typeof module !== "undefined" && module.exports) { readDedupedTokenUsage, getSummaryTitle, buildStepSummarySection, + buildWorkingSetDetailsSection, appendStepSummarySection, renderTokenTableAsPlainText, TOKEN_USAGE_AUDIT_PATH, diff --git a/actions/setup/js/parse_token_usage.test.cjs b/actions/setup/js/parse_token_usage.test.cjs index d936b0a8cf8..aca0ae0cbd5 100644 --- a/actions/setup/js/parse_token_usage.test.cjs +++ b/actions/setup/js/parse_token_usage.test.cjs @@ -12,6 +12,7 @@ const { readDedupedTokenUsage, getSummaryTitle, buildStepSummarySection, + buildWorkingSetDetailsSection, renderTokenTableAsPlainText, TOKEN_USAGE_AUDIT_PATH, TOKEN_USAGE_PATH, @@ -255,6 +256,8 @@ describe("parse_token_usage", () => { const stepSummary = originalReadFileSync(stepSummaryPath, "utf8"); expect(stepSummary).toContain("Token Usage"); expect(stepSummary).toContain("Per-request AI credits and token totals"); + expect(stepSummary).toContain("Working-Set Rebuild Factor (WSRF): 1.00× (measured)"); + expect(stepSummary).toContain("- Cumulative input tokens: 100"); expect(stepSummary).toContain("| ΔAI Credits | AI Credits |"); expect(fs.appendFileSync).toHaveBeenCalledWith(stepSummaryPath, expect.any(String), "utf8"); expect(mockCore.summary.addRaw).not.toHaveBeenCalled(); @@ -549,6 +552,37 @@ describe("parse_token_usage", () => { expect(section).toContain("Per-request AI credits and token totals"); }); + test("buildWorkingSetDetailsSection renders measured WSRF details", () => { + const section = buildWorkingSetDetailsSection({ + measurement_state: "measured", + rebuild_factor: 3.9017857142857144, + cumulative_input_tokens: 874000, + peak_input_tokens: 224000, + rebuild_excess_tokens: 650000, + invocations: 5, + }); + + expect(section).toContain("Working-Set Rebuild Factor (WSRF): 3.90× (measured)"); + expect(section).toContain("- State: `measured`"); + expect(section).toContain("- Cumulative input tokens: 874,000"); + expect(section).toContain("- Peak invocation input tokens: 224,000"); + expect(section).toContain("- Rebuild excess tokens: 650,000"); + expect(section).toContain("- Invocations: 5"); + }); + + test("buildWorkingSetDetailsSection renders unavailable state when no factor exists", () => { + const section = buildWorkingSetDetailsSection({ + measurement_state: "unavailable", + cumulative_input_tokens: 0, + peak_input_tokens: 0, + rebuild_excess_tokens: 0, + invocations: 0, + }); + + expect(section).toContain("Working-Set Rebuild Factor (WSRF): unavailable (unavailable)"); + expect(section).toContain("- State: `unavailable`"); + }); + test("renderTokenTableAsPlainText strips table separator lines and pipes", () => { const markdown = ["| # | Alias | Input | Output |", "|--:|-------|------:|-------:|", "| 1 | sonnet46 | 100 | 200 |", "| **Total** | | **100** | **200** |", "", "Legend: `Alias` is the model shorthand.", ""].join("\n"); From 2f2e6665d4fe7ca70e0c978c5b56d86d6c113bd1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 21 Aug 2026 18:33:59 +0000 Subject: [PATCH 2/2] Polish WSRF summary rendering Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/parse_token_usage.cjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/actions/setup/js/parse_token_usage.cjs b/actions/setup/js/parse_token_usage.cjs index 86966257e82..e9c74b5f6e3 100644 --- a/actions/setup/js/parse_token_usage.cjs +++ b/actions/setup/js/parse_token_usage.cjs @@ -116,8 +116,8 @@ function buildStepSummarySection(title, markdown, workingSet = null) { function buildWorkingSetDetailsSection(workingSet) { if (!workingSet || typeof workingSet !== "object") return ""; const measurementState = workingSet.measurement_state || "unavailable"; - const hasFactor = typeof workingSet.rebuild_factor === "number" && Number.isFinite(workingSet.rebuild_factor); - const displayFactor = hasFactor ? `${workingSet.rebuild_factor.toFixed(2)}×` : "unavailable"; + const rebuildFactor = typeof workingSet.rebuild_factor === "number" && Number.isFinite(workingSet.rebuild_factor) ? workingSet.rebuild_factor : null; + const displayFactor = rebuildFactor === null ? "unavailable" : `${rebuildFactor.toFixed(2)}×`; const displayInvocations = Number.isFinite(workingSet.invocations) ? workingSet.invocations.toLocaleString() : "0"; const displayCumulative = Number.isFinite(workingSet.cumulative_input_tokens) ? workingSet.cumulative_input_tokens.toLocaleString() : "0"; const displayPeak = Number.isFinite(workingSet.peak_input_tokens) ? workingSet.peak_input_tokens.toLocaleString() : "0";