From 50bf336b158458b4dec4e06647b6410b4cf64e69 Mon Sep 17 00:00:00 2001 From: Cam Quilici Date: Fri, 10 Jul 2026 12:35:16 -0500 Subject: [PATCH 1/4] fix(inference): scope run picker to selected scenario MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On the /inference dashboard, the "Run N/M" picker listed every workflow run for the date regardless of the selected scenario (Agentic Traces vs Single Turn): it filtered only by changelog config_keys (model + precision, no benchmark_type). A same-day single_turn sweep therefore leaked into the Agentic Traces picker, and selecting it poisoned the asOfRunId cutoff, blanking the chart. Fix, DB-side and non-circular: the runConfigs coverage in the workflow-info response (which enumerates all runs on a date independently of the asOf cutoff) now carries the raw benchmark_type / isl / osl columns. New pure helpers scenarioRunIdsForDate() (runEnumeration.ts) and restrictRunsToScenario() (utils.ts) derive the run ids with coverage for the selected model + scenario and restrict the picker list to them, with fallbacks so the selector still renders when coverage data is unavailable. GlobalFilterContext exposes runConfigs; InferenceContext filters the picker's runs by the selected scenario before deriving asOf. Filtering from loaded chart rows was rejected as circular (those rows depend on asOfRunId). Unofficial-run overlays are unaffected: dataRunsForDate() dedupes by run id, so the finer-grained DISTINCT rows collapse. 中文:修复 /inference 面板中 "Run N/M" 选择器不区分场景的问题。此前 选择器仅按 changelog 的 config_keys(模型 + 精度)过滤,不含 benchmark_type,导致同一天的 single_turn 运行泄漏进 Agentic Traces 的选择器,选中后会污染 asOfRunId 截断点、使图表变空。修复方案在数据 库侧且无循环依赖:workflow-info 响应中的 runConfigs 覆盖数据(独立于 asOf 截断点、枚举当天全部运行)新增 benchmark_type / isl / osl 原始 列;新增纯函数 scenarioRunIdsForDate()(runEnumeration.ts)和 restrictRunsToScenario()(utils.ts),按所选模型 + 场景推导有数据覆盖 的运行 id 并据此收窄选择器列表,覆盖数据缺失时回退到原列表以保证选择 器正常渲染。GlobalFilterContext 暴露 runConfigs;InferenceContext 在 推导 asOf 之前按所选场景过滤选择器的运行列表。基于已加载图表行的客户 端过滤方案因循环依赖(这些行本身依赖 asOfRunId)被否决。非官方运行 叠加层不受影响:dataRunsForDate() 按运行 id 去重,更细粒度的 DISTINCT 行会被合并。 Co-Authored-By: Claude Fable 5 --- packages/app/cypress/support/mock-data.ts | 1 + .../src/components/GlobalFilterContext.tsx | 14 +++- .../components/inference/InferenceContext.tsx | 39 +++++++-- .../inference/utils/runEnumeration.test.ts | 79 ++++++++++++++++++- .../inference/utils/runEnumeration.ts | 37 +++++++++ packages/app/src/lib/api.ts | 6 ++ packages/app/src/lib/utils.test.ts | 46 +++++++++++ packages/app/src/lib/utils.ts | 33 ++++++++ packages/db/src/json-provider.ts | 6 +- packages/db/src/queries/workflow-info.ts | 11 ++- 10 files changed, 261 insertions(+), 11 deletions(-) diff --git a/packages/app/cypress/support/mock-data.ts b/packages/app/cypress/support/mock-data.ts index bcad303fe..fdce609d7 100644 --- a/packages/app/cypress/support/mock-data.ts +++ b/packages/app/cypress/support/mock-data.ts @@ -447,6 +447,7 @@ export function createMockGlobalFilterContext( availabilityRows: undefined, workflowInfo: null, availableRuns: {}, + runConfigs: [], workflowLoading: false, workflowError: null, ...overrides, diff --git a/packages/app/src/components/GlobalFilterContext.tsx b/packages/app/src/components/GlobalFilterContext.tsx index 11a75324a..f51e4c604 100644 --- a/packages/app/src/components/GlobalFilterContext.tsx +++ b/packages/app/src/components/GlobalFilterContext.tsx @@ -36,7 +36,7 @@ import { import { computeAutoSwitchDecision } from '@/lib/unofficial-run-auto-switch'; import { countCurvesByPrecision, resolveEffectivePrecisions } from '@/lib/default-precisions'; import { resolveEffectiveSequence } from '@/lib/default-sequence'; -import type { AvailabilityRow, WorkflowInfoResponse } from '@/lib/api'; +import type { AvailabilityRow, RunConfigRow, WorkflowInfoResponse } from '@/lib/api'; const RUNDATE_RE = /^\d{4}-\d{2}-\d{2}$/u; const RUNID_RE = /^[A-Za-z0-9_-]{1,64}$/u; @@ -107,6 +107,14 @@ export interface GlobalFilterContextType { // Workflow info workflowInfo: { runInfoBySequence: Record }[] | null; availableRuns: Record; + /** + * Per-(run, config) coverage for the current date — which workflow runs + * produced benchmark data for which model / scenario / precision. Data-driven + * (from the benchmark rows), so it enumerates every run on the date including + * ones without a changelog entry. The run picker uses this to scope its list + * to the selected model + scenario (see `scenarioRunIdsForDate`). + */ + runConfigs: RunConfigRow[]; workflowLoading: boolean; workflowError: string | null; } @@ -442,6 +450,8 @@ export function GlobalFilterProvider({ [workflowData], ); + const runConfigs = useMemo(() => workflowData?.runConfigs ?? [], [workflowData]); + const workflowInfo = useMemo( () => (Object.keys(availableRuns).length > 0 ? [{ runInfoBySequence: availableRuns }] : null), [availableRuns], @@ -530,6 +540,7 @@ export function GlobalFilterProvider({ availabilityRows, workflowInfo, availableRuns, + runConfigs, workflowLoading, workflowError, }), @@ -551,6 +562,7 @@ export function GlobalFilterProvider({ availabilityRows, workflowInfo, availableRuns, + runConfigs, workflowLoading, workflowError, ], diff --git a/packages/app/src/components/inference/InferenceContext.tsx b/packages/app/src/components/inference/InferenceContext.tsx index e3738b563..a31878fce 100644 --- a/packages/app/src/components/inference/InferenceContext.tsx +++ b/packages/app/src/components/inference/InferenceContext.tsx @@ -63,7 +63,7 @@ import { resolveExclusionToggle, type ExclusionConflictPolicy, } from '@/lib/exclusion'; -import { filterRunsByModel, getDisplayLabel } from '@/lib/utils'; +import { filterRunsByModel, getDisplayLabel, restrictRunsToScenario } from '@/lib/utils'; import { isAgenticOnlyXAxisMode, @@ -72,6 +72,7 @@ import { type XAxisMode, } from './hooks/useChartData'; import { resolveComparisonEntries } from './utils/comparisonEntry'; +import { scenarioRunIdsForDate } from './utils/runEnumeration'; import { resolveLabelState, serializeLabelState } from './utils/label-defaults'; import { EMPTY_QUICK_FILTERS, @@ -135,6 +136,7 @@ export function InferenceProvider({ availabilityRows, workflowInfo, availableRuns, + runConfigs, workflowError, } = useGlobalFilters(); @@ -368,11 +370,39 @@ export function InferenceProvider({ [selectedModel], ); - const filteredAvailableRuns = useMemo( + // DB model keys for the selected display model (e.g. ['dsv4']). Used both by + // the scenario run scoping below and the GPU comparison date picker further down. + const dbModelKeys = useMemo( + () => DISPLAY_MODEL_TO_DB[selectedModel] ?? [selectedModel], + [selectedModel], + ); + + const changelogFilteredRuns = useMemo( () => filterRunsByModel(availableRuns, modelPrefixes, [...effectivePrecisions]), [availableRuns, modelPrefixes, effectivePrecisions], ); + // Run ids that actually produced data for the selected model + scenario on the + // date, derived from per-(run, config) coverage (independent of the chart's + // "as of run" query, so no circular dependency). `/api/v1/workflow-info` + // returns every run on a date across all models/scenarios, so without this a + // same-day run for a different scenario (e.g. a single_turn sweep while viewing + // Agentic Traces) would leak into the picker and poison the "as of run" cutoff. + const scenarioRunIds = useMemo( + () => + scenarioRunIdsForDate(runConfigs, dbModelKeys, effectiveSequence, [...effectivePrecisions]), + [runConfigs, dbModelKeys, effectiveSequence, effectivePrecisions], + ); + + // The picker list: the changelog/precision-scoped runs, further restricted to + // the ones that shipped data for the selected scenario. Falls back to the + // changelog-scoped list when coverage data is unavailable (old dates / JSON + // mode) so the selector always renders. + const filteredAvailableRuns = useMemo( + () => restrictRunsToScenario(changelogFilteredRuns, availableRuns, scenarioRunIds), + [changelogFilteredRuns, availableRuns, scenarioRunIds], + ); + const effectiveSelectedRunId = useMemo(() => { if (!filteredAvailableRuns) return selectedRunId; const filteredRunIds = Object.keys(filteredAvailableRuns); @@ -484,11 +514,6 @@ export function InferenceProvider({ ); // For GPU comparison date picker — use shared availability data from global filters - const dbModelKeys = useMemo( - () => DISPLAY_MODEL_TO_DB[selectedModel] ?? [selectedModel], - [selectedModel], - ); - const dateRangeAvailableDates = useMemo(() => { if (selectedGPUs.length === 0) return availableDates; if (!availabilityRows) return availableDates; diff --git a/packages/app/src/components/inference/utils/runEnumeration.test.ts b/packages/app/src/components/inference/utils/runEnumeration.test.ts index 155fae66a..6eb7e5ffb 100644 --- a/packages/app/src/components/inference/utils/runEnumeration.test.ts +++ b/packages/app/src/components/inference/utils/runEnumeration.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest'; import type { RunConfigRow } from '@/lib/api'; -import { dataRunsForDate } from './runEnumeration'; +import { dataRunsForDate, scenarioRunIdsForDate } from './runEnumeration'; function rc(over: Partial): RunConfigRow { return { @@ -16,10 +16,24 @@ function rc(over: Partial): RunConfigRow { framework: 'vllm', spec_method: 'none', disagg: false, + // Defaults to a single_turn 8k/1k row; agentic tests override these. + benchmark_type: 'single_turn', + isl: 8192, + osl: 1024, ...over, }; } +/** A single_turn (fixed-seq) run config for the given isl/osl. */ +function single(over: Partial): RunConfigRow { + return rc({ benchmark_type: 'single_turn', isl: 8192, osl: 1024, ...over }); +} + +/** An agentic_traces run config (null isl/osl, as ingested for agentic rows). */ +function agentic(over: Partial): RunConfigRow { + return rc({ benchmark_type: 'agentic_traces', isl: null, osl: null, ...over }); +} + const SCOPE = { modelDbKeys: ['minimaxm3'], selectedGPUs: ['mi300x_vllm'], @@ -107,3 +121,66 @@ describe('dataRunsForDate', () => { expect(dataRunsForDate([rc({ model: 'dsr1' })], SCOPE)).toEqual([]); }); }); + +// Mirrors the repro: on one date the same model has a single_turn run and an +// agentic run; each scenario must list ONLY its own run. +const ids = (rows: RunConfigRow[], model: string[], seq: string, prec: string[] = []) => + [...scenarioRunIdsForDate(rows, model, seq, prec)].toSorted(); + +describe('scenarioRunIdsForDate', () => { + it('agentic scenario excludes runs that only produced single_turn data', () => { + const rows = [ + agentic({ github_run_id: 28955639528, model: 'dsv4' }), // dsv4 agentic + single({ github_run_id: 28900000001, model: 'dsv4' }), // dsv4 single_turn (leaks today) + single({ github_run_id: 28900000002, model: 'glm5' }), // other model + ]; + expect(ids(rows, ['dsv4'], 'agentic-traces')).toEqual(['28955639528']); + }); + + it('single_turn scenario excludes agentic-only runs', () => { + const rows = [ + agentic({ github_run_id: 1, model: 'dsv4' }), + single({ github_run_id: 2, model: 'dsv4' }), + ]; + expect(ids(rows, ['dsv4'], '8k/1k')).toEqual(['2']); + }); + + it('lists a run that produced data for both scenarios under each scenario', () => { + const rows = [ + agentic({ github_run_id: 42, model: 'dsv4' }), + single({ github_run_id: 42, model: 'dsv4' }), // same run, both types + ]; + expect(ids(rows, ['dsv4'], 'agentic-traces')).toEqual(['42']); + expect(ids(rows, ['dsv4'], '8k/1k')).toEqual(['42']); + }); + + it('scopes to the selected model DB keys', () => { + const rows = [ + agentic({ github_run_id: 1, model: 'dsv4' }), + agentic({ github_run_id: 2, model: 'glm5' }), + ]; + expect(ids(rows, ['dsv4'], 'agentic-traces')).toEqual(['1']); + }); + + it('respects precision scoping when precisions are provided', () => { + const rows = [ + agentic({ github_run_id: 1, model: 'dsv4', precision: 'fp4' }), + agentic({ github_run_id: 2, model: 'dsv4', precision: 'fp8' }), + ]; + expect(ids(rows, ['dsv4'], 'agentic-traces', ['fp4'])).toEqual(['1']); + // Empty precisions = no precision constraint. + expect(ids(rows, ['dsv4'], 'agentic-traces')).toEqual(['1', '2']); + }); + + it('dedupes a run appearing across multiple matching configs', () => { + const rows = [ + agentic({ github_run_id: 7, model: 'dsv4', hardware: 'b200' }), + agentic({ github_run_id: 7, model: 'dsv4', hardware: 'gb200' }), + ]; + expect(ids(rows, ['dsv4'], 'agentic-traces')).toEqual(['7']); + }); + + it('returns an empty set when there is no coverage data', () => { + expect(scenarioRunIdsForDate([], ['dsv4'], 'agentic-traces').size).toBe(0); + }); +}); diff --git a/packages/app/src/components/inference/utils/runEnumeration.ts b/packages/app/src/components/inference/utils/runEnumeration.ts index fda1ae30f..7211d89e9 100644 --- a/packages/app/src/components/inference/utils/runEnumeration.ts +++ b/packages/app/src/components/inference/utils/runEnumeration.ts @@ -15,6 +15,8 @@ * chart keys them. */ +import { rowToSequence } from '@semianalysisai/inferencex-constants'; + import type { AggDataEntry } from '@/components/inference/types'; import type { RunConfigRow } from '@/lib/api'; import { getHardwareKey } from '@/lib/chart-utils'; @@ -78,3 +80,38 @@ export function dataRunsForDate(runConfigs: RunConfigRow[], scope: RunScope): Da return [...byRun.values()].toSorted((a, b) => a.runStartedAt.localeCompare(b.runStartedAt)); } + +/** + * GitHub run ids (as strings) that produced benchmark data for the given model + * DB keys + scenario (sequence) — and, when `precisions` is non-empty, only for + * those precisions — on a date. Derived from per-(run, config) coverage + * ({@link RunConfigRow}, i.e. the benchmark rows themselves), so a run that + * shipped data without a changelog entry still counts. + * + * The run picker uses this to list ONLY the runs that actually produced data for + * the currently selected model + scenario. `/api/v1/workflow-info` returns every + * run on a date across all models and scenarios, so without this scoping a + * same-day run for a different scenario (e.g. a `single_turn` sweep while the + * user is viewing Agentic Traces) leaks into the picker — and selecting it + * poisons the "as of run" cutoff, blanking the chart. + * + * Scenario matching mirrors the chart's own row filter (`rowToSequence(row) === + * selectedSequence` in useChartData), so the picker and the plotted data always + * agree on which runs are relevant. + */ +export function scenarioRunIdsForDate( + runConfigs: RunConfigRow[], + modelDbKeys: string[], + sequence: string, + precisions: string[] = [], +): Set { + const precSet = precisions.length > 0 ? new Set(precisions) : null; + const ids = new Set(); + for (const rc of runConfigs) { + if (!modelDbKeys.includes(rc.model)) continue; + if (precSet && !precSet.has(rc.precision)) continue; + if (rowToSequence(rc) !== sequence) continue; + ids.add(String(rc.github_run_id)); + } + return ids; +} diff --git a/packages/app/src/lib/api.ts b/packages/app/src/lib/api.ts index 270884558..eca840969 100644 --- a/packages/app/src/lib/api.ts +++ b/packages/app/src/lib/api.ts @@ -96,6 +96,12 @@ export interface RunConfigRow { framework: string; spec_method: string; disagg: boolean; + /** Benchmark scenario: `single_turn` (fixed-seq isl/osl) or `agentic_traces`. */ + benchmark_type: string; + /** ISL in tokens — null for agentic_traces. With osl + benchmark_type this maps to a sequence. */ + isl: number | null; + /** OSL in tokens — null for agentic_traces. */ + osl: number | null; } export interface WorkflowInfoResponse { diff --git a/packages/app/src/lib/utils.test.ts b/packages/app/src/lib/utils.test.ts index 13de88b72..6b0272c67 100644 --- a/packages/app/src/lib/utils.test.ts +++ b/packages/app/src/lib/utils.test.ts @@ -10,6 +10,7 @@ import { computeOutputCostFields, computeInputCostFields, filterRunsByModel, + restrictRunsToScenario, getFrameworkLabel, getHardwareLabel, getDisplayLabel, @@ -539,6 +540,51 @@ describe('filterRunsByModel', () => { }); }); +// --------------------------------------------------------------------------- +// restrictRunsToScenario +// --------------------------------------------------------------------------- +describe('restrictRunsToScenario', () => { + const runs = { + '10': makeRun([['dsv4-fp4-b200-sglang']], { runId: '10' }), // agentic run + '20': makeRun([['dsv4-fp4-b200-trt']], { runId: '20' }), // single_turn run + }; + + it('keeps only runs present in the scenario coverage set', () => { + const result = restrictRunsToScenario(runs, runs, new Set(['10'])); + expect(Object.keys(result!)).toEqual(['10']); + }); + + it('falls back to the changelog-scoped list when coverage is empty', () => { + const result = restrictRunsToScenario(runs, runs, new Set()); + expect(result).toBe(runs); + }); + + it('pulls a scenario run from the raw map when the changelog list omits it', () => { + // A run that shipped data without a changelog entry: filterRunsByModel drops + // it, but it is still a valid scenario run the picker must show. + const changelogFiltered = { '10': runs['10'] }; + const allRuns = runs; + const result = restrictRunsToScenario(changelogFiltered, allRuns, new Set(['10', '20'])); + expect(Object.keys(result!).toSorted()).toEqual(['10', '20']); + expect(result!['20']).toBe(runs['20']); + }); + + it('prefers the changelog-scoped entry over the raw one', () => { + const scoped = makeRun([['dsv4-fp4-b200-sglang']], { runId: '10', conclusion: 'scoped' }); + const result = restrictRunsToScenario({ '10': scoped }, runs, new Set(['10'])); + expect(result!['10']).toBe(scoped); + }); + + it('falls back to the changelog list when no scenario run survives', () => { + const result = restrictRunsToScenario(runs, runs, new Set(['999'])); + expect(result).toBe(runs); + }); + + it('returns null changelog list unchanged', () => { + expect(restrictRunsToScenario(null, null, new Set(['10']))).toBeNull(); + }); +}); + // =========================================================================== // getFrameworkLabel // =========================================================================== diff --git a/packages/app/src/lib/utils.ts b/packages/app/src/lib/utils.ts index 1321eb770..0a42309af 100644 --- a/packages/app/src/lib/utils.ts +++ b/packages/app/src/lib/utils.ts @@ -288,6 +288,39 @@ export function filterRunsByModel( return Object.keys(fallback).length > 0 ? fallback : null; } +/** + * Restrict the run-picker list to the runs that actually produced data for the + * currently selected scenario, given `scenarioRunIds` (the run ids with coverage + * for the selected model + scenario, from `scenarioRunIdsForDate`). + * + * `changelogFiltered` is the model/precision-scoped list (`filterRunsByModel`); + * `allRuns` is the raw, date-wide map. For each scenario run we prefer the + * changelog-scoped entry (its changelog is already precision-filtered) and fall + * back to the raw entry — a run can ship benchmark data without a changelog + * entry, so `filterRunsByModel` may omit it while it is still a legitimate + * scenario run the user must be able to select. + * + * Fallbacks that keep the selector rendering: + * - `scenarioRunIds` empty → no coverage data (dates predating per-run + * coverage, or JSON/fixtures mode): return the changelog-scoped list + * unchanged rather than blanking the picker. + * - Non-empty coverage but zero surviving runs (shouldn't happen in practice) + * → also fall back to the changelog-scoped list. + */ +export function restrictRunsToScenario( + changelogFiltered: Record | null, + allRuns: Record | null, + scenarioRunIds: Set, +): Record | null { + if (scenarioRunIds.size === 0) return changelogFiltered; + const restricted: Record = {}; + for (const runId of scenarioRunIds) { + const info = changelogFiltered?.[runId] ?? allRuns?.[runId]; + if (info) restricted[runId] = info; + } + return Object.keys(restricted).length > 0 ? restricted : changelogFiltered; +} + /** * Computes missing energy fields (jTotal, jOutput, jInput) at runtime. * This handles backwards compatibility with historical data that doesn't have these fields. diff --git a/packages/db/src/json-provider.ts b/packages/db/src/json-provider.ts index f6c626f03..5db079526 100644 --- a/packages/db/src/json-provider.ts +++ b/packages/db/src/json-provider.ts @@ -913,7 +913,8 @@ export function getRunConfigsByDate(date: string): RunConfigRow[] { const c = s.configs.get(br.config_id); if (!c) continue; - const key = `${wr.github_run_id}|${c.model}|${c.precision}|${c.hardware}|${c.framework}|${c.spec_method}|${c.disagg}`; + const benchmarkType = br.benchmark_type ?? 'single_turn'; + const key = `${wr.github_run_id}|${c.model}|${c.precision}|${c.hardware}|${c.framework}|${c.spec_method}|${c.disagg}|${benchmarkType}|${br.isl}|${br.osl}`; if (seen.has(key)) continue; seen.add(key); @@ -928,6 +929,9 @@ export function getRunConfigsByDate(date: string): RunConfigRow[] { framework: c.framework, spec_method: c.spec_method, disagg: c.disagg, + benchmark_type: benchmarkType, + isl: br.isl, + osl: br.osl, }); } diff --git a/packages/db/src/queries/workflow-info.ts b/packages/db/src/queries/workflow-info.ts index 01e13dd88..a83b167b6 100644 --- a/packages/db/src/queries/workflow-info.ts +++ b/packages/db/src/queries/workflow-info.ts @@ -76,6 +76,12 @@ export interface RunConfigRow { framework: string; spec_method: string; disagg: boolean; + /** Benchmark scenario: `single_turn` (fixed-seq isl/osl) or `agentic_traces`. */ + benchmark_type: string; + /** ISL in tokens — null for agentic_traces. Together with osl + benchmark_type this maps to a sequence. */ + isl: number | null; + /** OSL in tokens — null for agentic_traces. */ + osl: number | null; } /** @@ -96,7 +102,10 @@ export async function getRunConfigsByDate(sql: DbClient, date: string): Promise< c.hardware, c.framework, c.spec_method, - c.disagg + c.disagg, + br.benchmark_type, + br.isl, + br.osl FROM benchmark_results br JOIN configs c ON c.id = br.config_id JOIN latest_workflow_runs wr ON wr.id = br.workflow_run_id From 3ad2d0c836fe756b716c629e0bfc4f9f16796b79 Mon Sep 17 00:00:00 2001 From: Cam Quilici Date: Fri, 10 Jul 2026 12:46:48 -0500 Subject: [PATCH 2/4] fix(inference): hide run picker when the scenario has no runs on the date MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to the scenario-scoped run picker: the empty-coverage fallback in restrictRunsToScenario() fired whenever scenarioRunIds was empty, which conflated two cases — "no per-run coverage rows for the date" (old data, where falling back is right) and "coverage exists but zero runs for this scenario" (e.g. 2026-07-02 / 2026-07-04, where only single_turn sweeps ran). In the second case the picker degraded to the old leaky behavior and listed 6 / 3 single_turn runs under Agentic Traces. restrictRunsToScenario() now takes coverageKnown (runConfigs.length > 0): when coverage data is unavailable it still falls back to the changelog-scoped list, but when coverage exists and the scenario has no runs it returns null so the picker hides — matching the (empty) chart instead of offering meaningless "as of run" cutoffs. 中文:场景化运行选择器的后续修复:restrictRunsToScenario() 原先只要 scenarioRunIds 为空就触发回退,混淆了两种情况——"当天没有任何按运行 的覆盖数据行"(旧数据,回退是正确的)与"覆盖数据存在但该场景当天没有 任何运行"(如 2026-07-02 / 2026-07-04,当天只有 single_turn 运行)。 在第二种情况下选择器退化回旧的泄漏行为,在 Agentic Traces 场景下列出 6 / 3 个 single_turn 运行。现在 restrictRunsToScenario() 接收 coverageKnown(runConfigs.length > 0):覆盖数据缺失时仍回退到 changelog 范围列表;覆盖数据存在但该场景无运行时返回 null,选择器随之 隐藏——与(空的)图表保持一致,而不是提供无意义的 "as of run" 截断点。 Co-Authored-By: Claude Fable 5 --- .../components/inference/InferenceContext.tsx | 19 ++++++++--- packages/app/src/lib/utils.test.ts | 32 +++++++++++++------ packages/app/src/lib/utils.ts | 24 +++++++++----- 3 files changed, 52 insertions(+), 23 deletions(-) diff --git a/packages/app/src/components/inference/InferenceContext.tsx b/packages/app/src/components/inference/InferenceContext.tsx index a31878fce..71e504847 100644 --- a/packages/app/src/components/inference/InferenceContext.tsx +++ b/packages/app/src/components/inference/InferenceContext.tsx @@ -395,12 +395,21 @@ export function InferenceProvider({ ); // The picker list: the changelog/precision-scoped runs, further restricted to - // the ones that shipped data for the selected scenario. Falls back to the - // changelog-scoped list when coverage data is unavailable (old dates / JSON - // mode) so the selector always renders. + // the ones that shipped data for the selected scenario. When the date has + // coverage rows but none for this scenario (e.g. only single_turn sweeps ran + // that day while viewing Agentic Traces) the picker hides (null) rather than + // listing other-scenario runs; only when coverage data is unavailable + // altogether (old dates / JSON snapshots) does it fall back to the + // changelog-scoped list so the selector still renders. const filteredAvailableRuns = useMemo( - () => restrictRunsToScenario(changelogFilteredRuns, availableRuns, scenarioRunIds), - [changelogFilteredRuns, availableRuns, scenarioRunIds], + () => + restrictRunsToScenario( + changelogFilteredRuns, + availableRuns, + scenarioRunIds, + runConfigs.length > 0, + ), + [changelogFilteredRuns, availableRuns, scenarioRunIds, runConfigs], ); const effectiveSelectedRunId = useMemo(() => { diff --git a/packages/app/src/lib/utils.test.ts b/packages/app/src/lib/utils.test.ts index 6b0272c67..c0b4e35ba 100644 --- a/packages/app/src/lib/utils.test.ts +++ b/packages/app/src/lib/utils.test.ts @@ -550,38 +550,50 @@ describe('restrictRunsToScenario', () => { }; it('keeps only runs present in the scenario coverage set', () => { - const result = restrictRunsToScenario(runs, runs, new Set(['10'])); + const result = restrictRunsToScenario(runs, runs, new Set(['10']), true); expect(Object.keys(result!)).toEqual(['10']); }); - it('falls back to the changelog-scoped list when coverage is empty', () => { - const result = restrictRunsToScenario(runs, runs, new Set()); + it('falls back to the changelog-scoped list when coverage data is unavailable', () => { + // Old dates / JSON snapshots without per-run coverage rows: we can't say + // anything about scenarios, so the picker keeps the changelog-scoped list. + const result = restrictRunsToScenario(runs, runs, new Set(), false); expect(result).toBe(runs); }); + it('hides the picker when coverage exists but the scenario has no runs that date', () => { + // Repro: 2026-07-02 / 2026-07-04 had only single_turn runs; on the Agentic + // Traces scenario the picker must show NO runs (null), not fall back to + // listing the single_turn runs. + const result = restrictRunsToScenario(runs, runs, new Set(), true); + expect(result).toBeNull(); + }); + it('pulls a scenario run from the raw map when the changelog list omits it', () => { // A run that shipped data without a changelog entry: filterRunsByModel drops // it, but it is still a valid scenario run the picker must show. const changelogFiltered = { '10': runs['10'] }; const allRuns = runs; - const result = restrictRunsToScenario(changelogFiltered, allRuns, new Set(['10', '20'])); + const result = restrictRunsToScenario(changelogFiltered, allRuns, new Set(['10', '20']), true); expect(Object.keys(result!).toSorted()).toEqual(['10', '20']); expect(result!['20']).toBe(runs['20']); }); it('prefers the changelog-scoped entry over the raw one', () => { const scoped = makeRun([['dsv4-fp4-b200-sglang']], { runId: '10', conclusion: 'scoped' }); - const result = restrictRunsToScenario({ '10': scoped }, runs, new Set(['10'])); + const result = restrictRunsToScenario({ '10': scoped }, runs, new Set(['10']), true); expect(result!['10']).toBe(scoped); }); - it('falls back to the changelog list when no scenario run survives', () => { - const result = restrictRunsToScenario(runs, runs, new Set(['999'])); - expect(result).toBe(runs); + it('returns null when no scenario run survives the restriction', () => { + // Scenario runs exist in coverage but none resolve to a listable RunInfo + // (e.g. still in progress, so absent from the workflow-runs map). + const result = restrictRunsToScenario(runs, runs, new Set(['999']), true); + expect(result).toBeNull(); }); - it('returns null changelog list unchanged', () => { - expect(restrictRunsToScenario(null, null, new Set(['10']))).toBeNull(); + it('returns null changelog list unchanged when coverage is unavailable', () => { + expect(restrictRunsToScenario(null, null, new Set(), false)).toBeNull(); }); }); diff --git a/packages/app/src/lib/utils.ts b/packages/app/src/lib/utils.ts index 0a42309af..8a729216c 100644 --- a/packages/app/src/lib/utils.ts +++ b/packages/app/src/lib/utils.ts @@ -300,25 +300,33 @@ export function filterRunsByModel( * entry, so `filterRunsByModel` may omit it while it is still a legitimate * scenario run the user must be able to select. * - * Fallbacks that keep the selector rendering: - * - `scenarioRunIds` empty → no coverage data (dates predating per-run - * coverage, or JSON/fixtures mode): return the changelog-scoped list - * unchanged rather than blanking the picker. - * - Non-empty coverage but zero surviving runs (shouldn't happen in practice) - * → also fall back to the changelog-scoped list. + * `coverageKnown` says whether per-(run, config) coverage rows exist for the + * date at all (`runConfigs.length > 0`). It distinguishes two very different + * "empty `scenarioRunIds`" cases: + * - `coverageKnown === false` → no coverage data (dates predating per-run + * coverage, or JSON/fixtures snapshots without it): we can't say anything + * about scenarios, so return the changelog-scoped list unchanged rather + * than blanking the picker. + * - `coverageKnown === true` and no scenario run survives → the date has + * benchmark data but NONE for this scenario (e.g. only single_turn sweeps + * ran that day while the user views Agentic Traces). Return null so the + * picker hides instead of listing other-scenario runs — showing them is + * exactly the leak this helper exists to prevent, and selecting one would + * constrain an already-empty chart to a meaningless "as of run" cutoff. */ export function restrictRunsToScenario( changelogFiltered: Record | null, allRuns: Record | null, scenarioRunIds: Set, + coverageKnown: boolean, ): Record | null { - if (scenarioRunIds.size === 0) return changelogFiltered; + if (!coverageKnown) return changelogFiltered; const restricted: Record = {}; for (const runId of scenarioRunIds) { const info = changelogFiltered?.[runId] ?? allRuns?.[runId]; if (info) restricted[runId] = info; } - return Object.keys(restricted).length > 0 ? restricted : changelogFiltered; + return Object.keys(restricted).length > 0 ? restricted : null; } /** From 541bd33f8712c57876f9e6948a89964091b1235c Mon Sep 17 00:00:00 2001 From: Cam Quilici Date: Fri, 10 Jul 2026 13:14:44 -0500 Subject: [PATCH 3/4] fix(inference): resolve scenario run state at the provider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Filter workflow runs by model, scenario, and precision before global run auto-selection or URL synchronization. Invalid scenario dates and run IDs now resolve synchronously, so July 4 single-turn state cannot leak into Agentic Traces. Remove the downstream fallback-heavy filtering and add focused date and run-coverage regressions. 中文:在全局运行自动选择和 URL 同步之前,按模型、场景和精度过滤 workflow runs。无效的场景日期与运行 ID 现在会同步纠正,避免 7 月 4 日的 single-turn 状态泄漏到 Agentic Traces;同时移除下游复杂回退逻辑并补充定向回归测试。 --- packages/app/cypress/support/mock-data.ts | 1 - .../src/components/GlobalFilterContext.tsx | 63 +++++++++------- .../components/inference/InferenceContext.tsx | 50 ++----------- .../inference/utils/runEnumeration.test.ts | 75 +------------------ .../inference/utils/runEnumeration.ts | 37 --------- packages/app/src/lib/run-configs.test.ts | 58 ++++++++++++++ packages/app/src/lib/run-configs.ts | 21 ++++++ packages/app/src/lib/run-date.test.ts | 13 ++++ packages/app/src/lib/run-date.ts | 10 +++ packages/app/src/lib/utils.test.ts | 58 -------------- packages/app/src/lib/utils.ts | 41 ---------- 11 files changed, 145 insertions(+), 282 deletions(-) create mode 100644 packages/app/src/lib/run-configs.test.ts create mode 100644 packages/app/src/lib/run-configs.ts create mode 100644 packages/app/src/lib/run-date.test.ts create mode 100644 packages/app/src/lib/run-date.ts diff --git a/packages/app/cypress/support/mock-data.ts b/packages/app/cypress/support/mock-data.ts index fdce609d7..bcad303fe 100644 --- a/packages/app/cypress/support/mock-data.ts +++ b/packages/app/cypress/support/mock-data.ts @@ -447,7 +447,6 @@ export function createMockGlobalFilterContext( availabilityRows: undefined, workflowInfo: null, availableRuns: {}, - runConfigs: [], workflowLoading: false, workflowError: null, ...overrides, diff --git a/packages/app/src/components/GlobalFilterContext.tsx b/packages/app/src/components/GlobalFilterContext.tsx index f51e4c604..10cd3c01c 100644 --- a/packages/app/src/components/GlobalFilterContext.tsx +++ b/packages/app/src/components/GlobalFilterContext.tsx @@ -36,7 +36,9 @@ import { import { computeAutoSwitchDecision } from '@/lib/unofficial-run-auto-switch'; import { countCurvesByPrecision, resolveEffectivePrecisions } from '@/lib/default-precisions'; import { resolveEffectiveSequence } from '@/lib/default-sequence'; -import type { AvailabilityRow, RunConfigRow, WorkflowInfoResponse } from '@/lib/api'; +import { scenarioRunIdsForDate } from '@/lib/run-configs'; +import { resolveRunDate } from '@/lib/run-date'; +import type { AvailabilityRow, WorkflowInfoResponse } from '@/lib/api'; const RUNDATE_RE = /^\d{4}-\d{2}-\d{2}$/u; const RUNID_RE = /^[A-Za-z0-9_-]{1,64}$/u; @@ -107,14 +109,6 @@ export interface GlobalFilterContextType { // Workflow info workflowInfo: { runInfoBySequence: Record }[] | null; availableRuns: Record; - /** - * Per-(run, config) coverage for the current date — which workflow runs - * produced benchmark data for which model / scenario / precision. Data-driven - * (from the benchmark rows), so it enumerates every run on the date including - * ones without a changelog entry. The run picker uses this to scope its list - * to the selected model + scenario (see `scenarioRunIdsForDate`). - */ - runConfigs: RunConfigRow[]; workflowLoading: boolean; workflowError: string | null; } @@ -419,14 +413,10 @@ export function GlobalFilterProvider({ setSelectedRunDateRev((v) => v + 1); }, []); - const effectiveRunDate = useMemo(() => { - if (availableDates.length === 0) return selectedRunDate; - const latest = availableDates.at(-1)!; - if (userPickedDateRef.current && selectedRunDate && availableDates.includes(selectedRunDate)) { - return selectedRunDate; - } - return latest; - }, [availableDates, selectedRunDate]); + const effectiveRunDate = useMemo( + () => resolveRunDate(availableDates, selectedRunDate, userPickedDateRef.current), + [availableDates, selectedRunDate], + ); // Sync selectedRunDate state when effectiveRunDate changes useEffect(() => { @@ -445,18 +435,33 @@ export function GlobalFilterProvider({ const workflowError = workflowQueryError ? workflowQueryError.message : null; - const availableRuns = useMemo( - () => (workflowData ? buildRunInfo(workflowData) : {}), - [workflowData], - ); + const dateRuns = useMemo(() => (workflowData ? buildRunInfo(workflowData) : {}), [workflowData]); const runConfigs = useMemo(() => workflowData?.runConfigs ?? [], [workflowData]); + // Keep run selection and g_runid scenario-safe at their source. Filtering + // later in InferenceContext leaves the global single-turn run ID alive. + const availableRuns = useMemo(() => { + const runIds = scenarioRunIdsForDate( + runConfigs, + dbModelKeys, + effectiveSequence, + effectivePrecisions, + ); + return Object.fromEntries(Object.entries(dateRuns).filter(([runId]) => runIds.has(runId))); + }, [dateRuns, runConfigs, dbModelKeys, effectiveSequence, effectivePrecisions]); + const workflowInfo = useMemo( () => (Object.keys(availableRuns).length > 0 ? [{ runInfoBySequence: availableRuns }] : null), [availableRuns], ); + const effectiveSelectedRunId = useMemo(() => { + const runIds = Object.keys(availableRuns); + if (runIds.includes(selectedRunId)) return selectedRunId; + return runIds.reduce((latest, runId) => (runId > latest ? runId : latest), ''); + }, [availableRuns, selectedRunId]); + // Auto-select latest run ID when availableRuns change const urlInitRef = useRef({ runIdApplied: false }); @@ -494,8 +499,10 @@ export function GlobalFilterProvider({ } setUrlParams({ g_model: selectedModel, - g_rundate: selectedRunDate, - g_runid: selectedRunId, + // Never write a stale URL date after model/scenario availability has + // already resolved it to a valid date (e.g. Agentic + 2026-07-04). + g_rundate: effectiveRunDate, + g_runid: effectiveSelectedRunId, // Don't pin the sequence to the URL until it's resolved from real // availability — writing the pre-load placeholder (8k/1k) would clobber a // shared `?i_seq=agentic-traces` link before the model's availability @@ -507,8 +514,8 @@ export function GlobalFilterProvider({ }); }, [ selectedModel, - selectedRunDate, - selectedRunId, + effectiveRunDate, + effectiveSelectedRunId, effectiveSequence, sequenceResolved, effectivePrecisions, @@ -530,7 +537,7 @@ export function GlobalFilterProvider({ selectedRunDate: effectiveRunDate, setSelectedRunDate: setSelectedRunDateManual, selectedRunDateRev, - selectedRunId, + selectedRunId: effectiveSelectedRunId, setSelectedRunId, availableModels, availableSequences, @@ -540,7 +547,6 @@ export function GlobalFilterProvider({ availabilityRows, workflowInfo, availableRuns, - runConfigs, workflowLoading, workflowError, }), @@ -554,7 +560,7 @@ export function GlobalFilterProvider({ effectiveRunDate, setSelectedRunDateManual, selectedRunDateRev, - selectedRunId, + effectiveSelectedRunId, availableModels, availableSequences, availablePrecisions, @@ -562,7 +568,6 @@ export function GlobalFilterProvider({ availabilityRows, workflowInfo, availableRuns, - runConfigs, workflowLoading, workflowError, ], diff --git a/packages/app/src/components/inference/InferenceContext.tsx b/packages/app/src/components/inference/InferenceContext.tsx index 71e504847..d4c4edcb9 100644 --- a/packages/app/src/components/inference/InferenceContext.tsx +++ b/packages/app/src/components/inference/InferenceContext.tsx @@ -63,7 +63,7 @@ import { resolveExclusionToggle, type ExclusionConflictPolicy, } from '@/lib/exclusion'; -import { filterRunsByModel, getDisplayLabel, restrictRunsToScenario } from '@/lib/utils'; +import { filterRunsByModel, getDisplayLabel } from '@/lib/utils'; import { isAgenticOnlyXAxisMode, @@ -72,7 +72,6 @@ import { type XAxisMode, } from './hooks/useChartData'; import { resolveComparisonEntries } from './utils/comparisonEntry'; -import { scenarioRunIdsForDate } from './utils/runEnumeration'; import { resolveLabelState, serializeLabelState } from './utils/label-defaults'; import { EMPTY_QUICK_FILTERS, @@ -136,7 +135,6 @@ export function InferenceProvider({ availabilityRows, workflowInfo, availableRuns, - runConfigs, workflowError, } = useGlobalFilters(); @@ -370,50 +368,13 @@ export function InferenceProvider({ [selectedModel], ); - // DB model keys for the selected display model (e.g. ['dsv4']). Used both by - // the scenario run scoping below and the GPU comparison date picker further down. - const dbModelKeys = useMemo( - () => DISPLAY_MODEL_TO_DB[selectedModel] ?? [selectedModel], - [selectedModel], - ); - - const changelogFilteredRuns = useMemo( + const filteredAvailableRuns = useMemo( () => filterRunsByModel(availableRuns, modelPrefixes, [...effectivePrecisions]), [availableRuns, modelPrefixes, effectivePrecisions], ); - // Run ids that actually produced data for the selected model + scenario on the - // date, derived from per-(run, config) coverage (independent of the chart's - // "as of run" query, so no circular dependency). `/api/v1/workflow-info` - // returns every run on a date across all models/scenarios, so without this a - // same-day run for a different scenario (e.g. a single_turn sweep while viewing - // Agentic Traces) would leak into the picker and poison the "as of run" cutoff. - const scenarioRunIds = useMemo( - () => - scenarioRunIdsForDate(runConfigs, dbModelKeys, effectiveSequence, [...effectivePrecisions]), - [runConfigs, dbModelKeys, effectiveSequence, effectivePrecisions], - ); - - // The picker list: the changelog/precision-scoped runs, further restricted to - // the ones that shipped data for the selected scenario. When the date has - // coverage rows but none for this scenario (e.g. only single_turn sweeps ran - // that day while viewing Agentic Traces) the picker hides (null) rather than - // listing other-scenario runs; only when coverage data is unavailable - // altogether (old dates / JSON snapshots) does it fall back to the - // changelog-scoped list so the selector still renders. - const filteredAvailableRuns = useMemo( - () => - restrictRunsToScenario( - changelogFilteredRuns, - availableRuns, - scenarioRunIds, - runConfigs.length > 0, - ), - [changelogFilteredRuns, availableRuns, scenarioRunIds, runConfigs], - ); - const effectiveSelectedRunId = useMemo(() => { - if (!filteredAvailableRuns) return selectedRunId; + if (!filteredAvailableRuns) return ''; const filteredRunIds = Object.keys(filteredAvailableRuns); if (filteredRunIds.length === 0 || filteredRunIds.includes(selectedRunId)) return selectedRunId; return filteredRunIds.reduce((max, id) => (id > max ? id : max), filteredRunIds[0]); @@ -523,6 +484,11 @@ export function InferenceProvider({ ); // For GPU comparison date picker — use shared availability data from global filters + const dbModelKeys = useMemo( + () => DISPLAY_MODEL_TO_DB[selectedModel] ?? [selectedModel], + [selectedModel], + ); + const dateRangeAvailableDates = useMemo(() => { if (selectedGPUs.length === 0) return availableDates; if (!availabilityRows) return availableDates; diff --git a/packages/app/src/components/inference/utils/runEnumeration.test.ts b/packages/app/src/components/inference/utils/runEnumeration.test.ts index 6eb7e5ffb..d1953c73b 100644 --- a/packages/app/src/components/inference/utils/runEnumeration.test.ts +++ b/packages/app/src/components/inference/utils/runEnumeration.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest'; import type { RunConfigRow } from '@/lib/api'; -import { dataRunsForDate, scenarioRunIdsForDate } from './runEnumeration'; +import { dataRunsForDate } from './runEnumeration'; function rc(over: Partial): RunConfigRow { return { @@ -24,16 +24,6 @@ function rc(over: Partial): RunConfigRow { }; } -/** A single_turn (fixed-seq) run config for the given isl/osl. */ -function single(over: Partial): RunConfigRow { - return rc({ benchmark_type: 'single_turn', isl: 8192, osl: 1024, ...over }); -} - -/** An agentic_traces run config (null isl/osl, as ingested for agentic rows). */ -function agentic(over: Partial): RunConfigRow { - return rc({ benchmark_type: 'agentic_traces', isl: null, osl: null, ...over }); -} - const SCOPE = { modelDbKeys: ['minimaxm3'], selectedGPUs: ['mi300x_vllm'], @@ -121,66 +111,3 @@ describe('dataRunsForDate', () => { expect(dataRunsForDate([rc({ model: 'dsr1' })], SCOPE)).toEqual([]); }); }); - -// Mirrors the repro: on one date the same model has a single_turn run and an -// agentic run; each scenario must list ONLY its own run. -const ids = (rows: RunConfigRow[], model: string[], seq: string, prec: string[] = []) => - [...scenarioRunIdsForDate(rows, model, seq, prec)].toSorted(); - -describe('scenarioRunIdsForDate', () => { - it('agentic scenario excludes runs that only produced single_turn data', () => { - const rows = [ - agentic({ github_run_id: 28955639528, model: 'dsv4' }), // dsv4 agentic - single({ github_run_id: 28900000001, model: 'dsv4' }), // dsv4 single_turn (leaks today) - single({ github_run_id: 28900000002, model: 'glm5' }), // other model - ]; - expect(ids(rows, ['dsv4'], 'agentic-traces')).toEqual(['28955639528']); - }); - - it('single_turn scenario excludes agentic-only runs', () => { - const rows = [ - agentic({ github_run_id: 1, model: 'dsv4' }), - single({ github_run_id: 2, model: 'dsv4' }), - ]; - expect(ids(rows, ['dsv4'], '8k/1k')).toEqual(['2']); - }); - - it('lists a run that produced data for both scenarios under each scenario', () => { - const rows = [ - agentic({ github_run_id: 42, model: 'dsv4' }), - single({ github_run_id: 42, model: 'dsv4' }), // same run, both types - ]; - expect(ids(rows, ['dsv4'], 'agentic-traces')).toEqual(['42']); - expect(ids(rows, ['dsv4'], '8k/1k')).toEqual(['42']); - }); - - it('scopes to the selected model DB keys', () => { - const rows = [ - agentic({ github_run_id: 1, model: 'dsv4' }), - agentic({ github_run_id: 2, model: 'glm5' }), - ]; - expect(ids(rows, ['dsv4'], 'agentic-traces')).toEqual(['1']); - }); - - it('respects precision scoping when precisions are provided', () => { - const rows = [ - agentic({ github_run_id: 1, model: 'dsv4', precision: 'fp4' }), - agentic({ github_run_id: 2, model: 'dsv4', precision: 'fp8' }), - ]; - expect(ids(rows, ['dsv4'], 'agentic-traces', ['fp4'])).toEqual(['1']); - // Empty precisions = no precision constraint. - expect(ids(rows, ['dsv4'], 'agentic-traces')).toEqual(['1', '2']); - }); - - it('dedupes a run appearing across multiple matching configs', () => { - const rows = [ - agentic({ github_run_id: 7, model: 'dsv4', hardware: 'b200' }), - agentic({ github_run_id: 7, model: 'dsv4', hardware: 'gb200' }), - ]; - expect(ids(rows, ['dsv4'], 'agentic-traces')).toEqual(['7']); - }); - - it('returns an empty set when there is no coverage data', () => { - expect(scenarioRunIdsForDate([], ['dsv4'], 'agentic-traces').size).toBe(0); - }); -}); diff --git a/packages/app/src/components/inference/utils/runEnumeration.ts b/packages/app/src/components/inference/utils/runEnumeration.ts index 7211d89e9..fda1ae30f 100644 --- a/packages/app/src/components/inference/utils/runEnumeration.ts +++ b/packages/app/src/components/inference/utils/runEnumeration.ts @@ -15,8 +15,6 @@ * chart keys them. */ -import { rowToSequence } from '@semianalysisai/inferencex-constants'; - import type { AggDataEntry } from '@/components/inference/types'; import type { RunConfigRow } from '@/lib/api'; import { getHardwareKey } from '@/lib/chart-utils'; @@ -80,38 +78,3 @@ export function dataRunsForDate(runConfigs: RunConfigRow[], scope: RunScope): Da return [...byRun.values()].toSorted((a, b) => a.runStartedAt.localeCompare(b.runStartedAt)); } - -/** - * GitHub run ids (as strings) that produced benchmark data for the given model - * DB keys + scenario (sequence) — and, when `precisions` is non-empty, only for - * those precisions — on a date. Derived from per-(run, config) coverage - * ({@link RunConfigRow}, i.e. the benchmark rows themselves), so a run that - * shipped data without a changelog entry still counts. - * - * The run picker uses this to list ONLY the runs that actually produced data for - * the currently selected model + scenario. `/api/v1/workflow-info` returns every - * run on a date across all models and scenarios, so without this scoping a - * same-day run for a different scenario (e.g. a `single_turn` sweep while the - * user is viewing Agentic Traces) leaks into the picker — and selecting it - * poisons the "as of run" cutoff, blanking the chart. - * - * Scenario matching mirrors the chart's own row filter (`rowToSequence(row) === - * selectedSequence` in useChartData), so the picker and the plotted data always - * agree on which runs are relevant. - */ -export function scenarioRunIdsForDate( - runConfigs: RunConfigRow[], - modelDbKeys: string[], - sequence: string, - precisions: string[] = [], -): Set { - const precSet = precisions.length > 0 ? new Set(precisions) : null; - const ids = new Set(); - for (const rc of runConfigs) { - if (!modelDbKeys.includes(rc.model)) continue; - if (precSet && !precSet.has(rc.precision)) continue; - if (rowToSequence(rc) !== sequence) continue; - ids.add(String(rc.github_run_id)); - } - return ids; -} diff --git a/packages/app/src/lib/run-configs.test.ts b/packages/app/src/lib/run-configs.test.ts new file mode 100644 index 000000000..f3444b5fd --- /dev/null +++ b/packages/app/src/lib/run-configs.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, it } from 'vitest'; + +import type { RunConfigRow } from './api'; +import { scenarioRunIdsForDate } from './run-configs'; + +function row(overrides: Partial): RunConfigRow { + return { + github_run_id: 1, + run_started_at: '2026-07-10T00:00:00Z', + html_url: null, + head_sha: null, + model: 'dsv4', + precision: 'fp4', + hardware: 'b200', + framework: 'sglang', + spec_method: 'none', + disagg: false, + benchmark_type: 'agentic_traces', + isl: null, + osl: null, + ...overrides, + }; +} + +const ids = (rows: RunConfigRow[], sequence: string) => + [...scenarioRunIdsForDate(rows, ['dsv4'], sequence, ['fp4'])].toSorted(); + +describe('scenarioRunIdsForDate', () => { + it('does not expose July 4 single-turn runs under Agentic Traces', () => { + const july4 = row({ + github_run_id: 28593351944, + benchmark_type: 'single_turn', + isl: 8192, + osl: 1024, + }); + expect(ids([july4], 'agentic-traces')).toEqual([]); + expect(ids([july4], '8k/1k')).toEqual(['28593351944']); + }); + + it('includes a run in each scenario for which it produced data', () => { + const rows = [ + row({ github_run_id: 42 }), + row({ github_run_id: 42, benchmark_type: 'single_turn', isl: 8192, osl: 1024 }), + ]; + expect(ids(rows, 'agentic-traces')).toEqual(['42']); + expect(ids(rows, '8k/1k')).toEqual(['42']); + }); + + it('filters by model and precision and deduplicates run IDs', () => { + const rows = [ + row({ github_run_id: 7 }), + row({ github_run_id: 7, hardware: 'b300' }), + row({ github_run_id: 8, model: 'glm5' }), + row({ github_run_id: 9, precision: 'fp8' }), + ]; + expect(ids(rows, 'agentic-traces')).toEqual(['7']); + }); +}); diff --git a/packages/app/src/lib/run-configs.ts b/packages/app/src/lib/run-configs.ts new file mode 100644 index 000000000..2eac8cc7c --- /dev/null +++ b/packages/app/src/lib/run-configs.ts @@ -0,0 +1,21 @@ +import { rowToSequence } from '@semianalysisai/inferencex-constants'; + +import type { RunConfigRow } from './api'; + +/** Run IDs with benchmark rows for the selected model, scenario, and precision. */ +export function scenarioRunIdsForDate( + runConfigs: RunConfigRow[], + modelDbKeys: string[], + sequence: string, + precisions: string[] = [], +): Set { + const selectedPrecisions = precisions.length > 0 ? new Set(precisions) : null; + const ids = new Set(); + for (const row of runConfigs) { + if (!modelDbKeys.includes(row.model)) continue; + if (selectedPrecisions && !selectedPrecisions.has(row.precision)) continue; + if (rowToSequence(row) !== sequence) continue; + ids.add(String(row.github_run_id)); + } + return ids; +} diff --git a/packages/app/src/lib/run-date.test.ts b/packages/app/src/lib/run-date.test.ts new file mode 100644 index 000000000..2b0ad7a36 --- /dev/null +++ b/packages/app/src/lib/run-date.test.ts @@ -0,0 +1,13 @@ +import { describe, expect, it } from 'vitest'; + +import { resolveRunDate } from './run-date'; + +describe('resolveRunDate', () => { + it('replaces a stale single-turn date with the latest agentic date', () => { + expect(resolveRunDate(['2026-07-09', '2026-07-10'], '2026-07-04', true)).toBe('2026-07-10'); + }); + + it('preserves an available date chosen by the user', () => { + expect(resolveRunDate(['2026-07-09', '2026-07-10'], '2026-07-09', true)).toBe('2026-07-09'); + }); +}); diff --git a/packages/app/src/lib/run-date.ts b/packages/app/src/lib/run-date.ts new file mode 100644 index 000000000..75e5b1402 --- /dev/null +++ b/packages/app/src/lib/run-date.ts @@ -0,0 +1,10 @@ +/** Resolve a selected run date against the dates available for the active scenario. */ +export function resolveRunDate( + availableDates: string[], + selectedDate: string, + preserveSelected: boolean, +): string { + if (availableDates.length === 0) return selectedDate; + if (preserveSelected && availableDates.includes(selectedDate)) return selectedDate; + return availableDates.at(-1)!; +} diff --git a/packages/app/src/lib/utils.test.ts b/packages/app/src/lib/utils.test.ts index c0b4e35ba..13de88b72 100644 --- a/packages/app/src/lib/utils.test.ts +++ b/packages/app/src/lib/utils.test.ts @@ -10,7 +10,6 @@ import { computeOutputCostFields, computeInputCostFields, filterRunsByModel, - restrictRunsToScenario, getFrameworkLabel, getHardwareLabel, getDisplayLabel, @@ -540,63 +539,6 @@ describe('filterRunsByModel', () => { }); }); -// --------------------------------------------------------------------------- -// restrictRunsToScenario -// --------------------------------------------------------------------------- -describe('restrictRunsToScenario', () => { - const runs = { - '10': makeRun([['dsv4-fp4-b200-sglang']], { runId: '10' }), // agentic run - '20': makeRun([['dsv4-fp4-b200-trt']], { runId: '20' }), // single_turn run - }; - - it('keeps only runs present in the scenario coverage set', () => { - const result = restrictRunsToScenario(runs, runs, new Set(['10']), true); - expect(Object.keys(result!)).toEqual(['10']); - }); - - it('falls back to the changelog-scoped list when coverage data is unavailable', () => { - // Old dates / JSON snapshots without per-run coverage rows: we can't say - // anything about scenarios, so the picker keeps the changelog-scoped list. - const result = restrictRunsToScenario(runs, runs, new Set(), false); - expect(result).toBe(runs); - }); - - it('hides the picker when coverage exists but the scenario has no runs that date', () => { - // Repro: 2026-07-02 / 2026-07-04 had only single_turn runs; on the Agentic - // Traces scenario the picker must show NO runs (null), not fall back to - // listing the single_turn runs. - const result = restrictRunsToScenario(runs, runs, new Set(), true); - expect(result).toBeNull(); - }); - - it('pulls a scenario run from the raw map when the changelog list omits it', () => { - // A run that shipped data without a changelog entry: filterRunsByModel drops - // it, but it is still a valid scenario run the picker must show. - const changelogFiltered = { '10': runs['10'] }; - const allRuns = runs; - const result = restrictRunsToScenario(changelogFiltered, allRuns, new Set(['10', '20']), true); - expect(Object.keys(result!).toSorted()).toEqual(['10', '20']); - expect(result!['20']).toBe(runs['20']); - }); - - it('prefers the changelog-scoped entry over the raw one', () => { - const scoped = makeRun([['dsv4-fp4-b200-sglang']], { runId: '10', conclusion: 'scoped' }); - const result = restrictRunsToScenario({ '10': scoped }, runs, new Set(['10']), true); - expect(result!['10']).toBe(scoped); - }); - - it('returns null when no scenario run survives the restriction', () => { - // Scenario runs exist in coverage but none resolve to a listable RunInfo - // (e.g. still in progress, so absent from the workflow-runs map). - const result = restrictRunsToScenario(runs, runs, new Set(['999']), true); - expect(result).toBeNull(); - }); - - it('returns null changelog list unchanged when coverage is unavailable', () => { - expect(restrictRunsToScenario(null, null, new Set(), false)).toBeNull(); - }); -}); - // =========================================================================== // getFrameworkLabel // =========================================================================== diff --git a/packages/app/src/lib/utils.ts b/packages/app/src/lib/utils.ts index 8a729216c..1321eb770 100644 --- a/packages/app/src/lib/utils.ts +++ b/packages/app/src/lib/utils.ts @@ -288,47 +288,6 @@ export function filterRunsByModel( return Object.keys(fallback).length > 0 ? fallback : null; } -/** - * Restrict the run-picker list to the runs that actually produced data for the - * currently selected scenario, given `scenarioRunIds` (the run ids with coverage - * for the selected model + scenario, from `scenarioRunIdsForDate`). - * - * `changelogFiltered` is the model/precision-scoped list (`filterRunsByModel`); - * `allRuns` is the raw, date-wide map. For each scenario run we prefer the - * changelog-scoped entry (its changelog is already precision-filtered) and fall - * back to the raw entry — a run can ship benchmark data without a changelog - * entry, so `filterRunsByModel` may omit it while it is still a legitimate - * scenario run the user must be able to select. - * - * `coverageKnown` says whether per-(run, config) coverage rows exist for the - * date at all (`runConfigs.length > 0`). It distinguishes two very different - * "empty `scenarioRunIds`" cases: - * - `coverageKnown === false` → no coverage data (dates predating per-run - * coverage, or JSON/fixtures snapshots without it): we can't say anything - * about scenarios, so return the changelog-scoped list unchanged rather - * than blanking the picker. - * - `coverageKnown === true` and no scenario run survives → the date has - * benchmark data but NONE for this scenario (e.g. only single_turn sweeps - * ran that day while the user views Agentic Traces). Return null so the - * picker hides instead of listing other-scenario runs — showing them is - * exactly the leak this helper exists to prevent, and selecting one would - * constrain an already-empty chart to a meaningless "as of run" cutoff. - */ -export function restrictRunsToScenario( - changelogFiltered: Record | null, - allRuns: Record | null, - scenarioRunIds: Set, - coverageKnown: boolean, -): Record | null { - if (!coverageKnown) return changelogFiltered; - const restricted: Record = {}; - for (const runId of scenarioRunIds) { - const info = changelogFiltered?.[runId] ?? allRuns?.[runId]; - if (info) restricted[runId] = info; - } - return Object.keys(restricted).length > 0 ? restricted : null; -} - /** * Computes missing energy fields (jTotal, jOutput, jInput) at runtime. * This handles backwards compatibility with historical data that doesn't have these fields. From 181a514385f6d72d3814c035318c5d920e3a74bf Mon Sep 17 00:00:00 2001 From: Cam Quilici Date: Fri, 10 Jul 2026 13:20:07 -0500 Subject: [PATCH 4/4] fix(api): invalidate stale workflow-info schema cache MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Version the workflow-info cache key after adding benchmark scenario fields. This prevents Vercel previews from reusing cached pre-change runConfigs rows that lack benchmark_type, isl, and osl and would otherwise hide the valid July 10 Agentic run and changelog. 中文:新增 benchmark 场景字段后升级 workflow-info 缓存键,避免 Vercel preview 复用缺少 benchmark_type、isl、osl 的旧 runConfigs 缓存,从而错误隐藏 7 月 10 日的 Agentic 运行与 changelog。 --- packages/app/src/app/api/v1/workflow-info/route.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/app/api/v1/workflow-info/route.ts b/packages/app/src/app/api/v1/workflow-info/route.ts index fc17db31c..dfb0234cc 100644 --- a/packages/app/src/app/api/v1/workflow-info/route.ts +++ b/packages/app/src/app/api/v1/workflow-info/route.ts @@ -31,7 +31,7 @@ const getCachedWorkflowInfo = cachedQuery(async (date: string) => { getRunConfigsByDate(sql, date), ]); return { runs, changelogs, configs, runConfigs }; -}, 'workflow-info'); +}, 'workflow-info-v2'); export async function GET(request: NextRequest) { const date = request.nextUrl.searchParams.get('date') ?? '';