Files
shockbot/utils/patchWorkflowRunFields.ts
T
Colin McDonnell 9d04cad360 drop legacy summaryCommentNodeId column (#617)
Was retained on `workflow_runs` after PR #568 replaced the comment-based
summary path with the snapshot architecture, with a "kept for backfill of
pre-snapshot runs" annotation. No backfill is planned: pre-snapshot summary
comments were written in the user-facing PR_SUMMARY_FORMAT (TL;DR + key
changes blockquote + before/after sections), not the agent-context
functional-summary format the snapshot now expects. Backfilling them would
prime new runs with the wrong shape and pollute the agent context. Old
comments stay on github.com as historical artifacts; the column on the DB
row is dead weight.

Strips the field from:
- prisma schema + new migration `20260508190000_drop_summary_comment_node_id`
- `app/api/workflow-run/[runId]/route.ts` STRING_FIELDS allowlist
- `action/utils/patchWorkflowRunFields.ts` type union + STRING_KEYS
- `utils/db/selectActiveWorkflowRuns.ts` select clause
- `utils/github/enrichWorkflowRunsWithArtifactUrls.ts` node-id type, URL
  resolution, collectUniqueNodeIds + urlsForRun
- `utils/webhooks/handleWorkflowRunWebhook.ts` two select clauses, the
  hasRecordedArtifact param, and the orphaned-leaping-comment alert text
- `components/RunArtifactPills.tsx` ArtifactKey union + ARTIFACT_KEYS +
  switch cases (drops the "View summary" chip from the workflow run list)

Verified: pnpm typecheck clean, pnpm lint clean (537 files), action build
clean. Dev DB reset against production parent and the migration applied
cleanly — column is gone from the workflow_runs table.
2026-05-08 19:47:38 +00:00

151 lines
5.0 KiB
TypeScript

import type { AgentUsage } from "../agents/shared.ts";
import type { ToolContext } from "../mcp/server.ts";
import { apiFetch } from "./apiFetch.ts";
import { log } from "./cli.ts";
import { retry } from "./retry.ts";
/**
* Artifact tracking fields — one-off PATCHes from MCP tools as GitHub entities
* are created during the run. Strings only (GraphQL node IDs).
* Keep in sync with `STRING_FIELDS` in `app/api/workflow-run/[runId]/route.ts`.
*/
export type WorkflowRunArtifactPatchKey =
| "prNodeId"
| "issueNodeId"
| "reviewNodeId"
| "planCommentNodeId"
| "summarySnapshot";
/**
* Usage fields — aggregated across all agent calls and PATCHed once at
* end-of-run. Token counts are Int4 on the DB side (ample for any realistic
* run); `costUsd` is a Decimal populated by provider-reported dollar amounts.
* Keep in sync with `INT_FIELDS` + `DECIMAL_FIELDS` in the server route.
*/
export type WorkflowRunUsagePatchKey =
| "inputTokens"
| "outputTokens"
| "cacheReadTokens"
| "cacheWriteTokens"
| "costUsd";
export type WorkflowRunPatch = Partial<Record<WorkflowRunArtifactPatchKey, string>> &
Partial<Record<WorkflowRunUsagePatchKey, number>>;
const STRING_KEYS: WorkflowRunArtifactPatchKey[] = [
"prNodeId",
"issueNodeId",
"reviewNodeId",
"planCommentNodeId",
"summarySnapshot",
];
const NUMBER_KEYS: WorkflowRunUsagePatchKey[] = [
"inputTokens",
"outputTokens",
"cacheReadTokens",
"cacheWriteTokens",
"costUsd",
];
/** PATCH workflow-run fields (Pullfrog JWT, not GitHub). */
export async function patchWorkflowRunFields(
ctx: ToolContext,
fields: WorkflowRunPatch
): Promise<void> {
if (ctx.runId === undefined || !ctx.apiToken) return;
const body: Record<string, string | number> = {};
for (const key of STRING_KEYS) {
const value = fields[key];
if (typeof value === "string" && value.length > 0) {
body[key] = value;
}
}
for (const key of NUMBER_KEYS) {
const value = fields[key];
if (typeof value === "number" && Number.isFinite(value) && value >= 0) {
body[key] = value;
}
}
if (Object.keys(body).length === 0) return;
try {
await retry(
async () => {
const response = await apiFetch({
path: `/api/workflow-run/${ctx.runId}`,
method: "PATCH",
headers: {
authorization: `Bearer ${ctx.apiToken}`,
"content-type": "application/json",
},
body: JSON.stringify(body),
signal: AbortSignal.timeout(10_000),
});
if (!response.ok) throw new Error(`PATCH workflow-run: ${response.status}`);
},
{
maxAttempts: 3,
delayMs: 2000,
label: "patchWorkflowRunFields",
}
);
} catch (error) {
log.warning(`patchWorkflowRunFields exhausted retries: ${error}`);
}
}
/**
* Postgres INTEGER / Prisma Int4 is signed 32-bit. Aggregated usage won't
* realistically hit this in a single run (2.1B tokens ≈ $6000+ of input on
* Claude Opus), but clamping here keeps the wire payload self-consistent:
* the server rejects out-of-range INT fields individually, so without a
* client-side clamp a single overflow would write a partial row where
* some columns land and others silently don't.
*/
const INT4_MAX = 2_147_483_647;
function clampInt(value: number, field: WorkflowRunUsagePatchKey): number {
if (value > INT4_MAX) {
log.warning(
`aggregateUsage: ${field}=${value} exceeds INT4_MAX (${INT4_MAX}) — clamping so the rest of the usage row still persists.`
);
return INT4_MAX;
}
return value;
}
/**
* Sum per-agent usage entries into a single WorkflowRunPatch payload.
* Returns an empty object when there's nothing to report, which causes
* `patchWorkflowRunFields` to no-op — safe to call unconditionally from
* end-of-run paths. Zero-valued fields are dropped so the DB only stores
* positive sums (and NULL means "not reported").
*
* Token sums are clamped to INT4_MAX to guarantee the payload the server
* sees is always self-consistent across all numeric columns.
*/
export function aggregateUsage(entries: AgentUsage[]): WorkflowRunPatch {
if (entries.length === 0) return {};
const sum = entries.reduce(
(acc, e) => ({
inputTokens: acc.inputTokens + e.inputTokens,
outputTokens: acc.outputTokens + e.outputTokens,
cacheReadTokens: acc.cacheReadTokens + (e.cacheReadTokens ?? 0),
cacheWriteTokens: acc.cacheWriteTokens + (e.cacheWriteTokens ?? 0),
costUsd: acc.costUsd + (e.costUsd ?? 0),
}),
{ inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, costUsd: 0 }
);
const out: WorkflowRunPatch = {};
if (sum.inputTokens > 0) out.inputTokens = clampInt(sum.inputTokens, "inputTokens");
if (sum.outputTokens > 0) out.outputTokens = clampInt(sum.outputTokens, "outputTokens");
if (sum.cacheReadTokens > 0)
out.cacheReadTokens = clampInt(sum.cacheReadTokens, "cacheReadTokens");
if (sum.cacheWriteTokens > 0)
out.cacheWriteTokens = clampInt(sum.cacheWriteTokens, "cacheWriteTokens");
if (sum.costUsd > 0) out.costUsd = sum.costUsd;
return out;
}