Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions docs/architecture/02_runtime_lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,10 @@ benchmark-run-start ─► workflow/started │ f
workflow-complete / workflow-failed
• close RunRecord
• emit run/cleanup
• emit sample/cleanup
run-cleanup
sample-cleanup
• terminate external sandbox if present
• reconcile RunRecord.status
```
Expand All @@ -109,9 +109,9 @@ Dashboard delivery hangs off state mutation (see `05_dashboard.md`); it is not a

6. **Cascade cancellation is one transaction, not an event chain.** `SubtaskCancellationService.cancel_orphans` walks the entire descendant subtree via BFS on `parent_node_id` in a single DB transaction (`subtask_cancellation_service.py:66-111`). A dropped or delayed Inngest event cannot leave a grandchild running under a cancelled parent. The subsequent `task/cancelled` events are for per-node cleanup, not recursion.

7. **Sandbox lifecycle is per-task, teardown happens after evaluators.** `task-execute` invokes `sandbox-setup`, then keeps that sandbox id alive through worker execution, output persistence, evaluator fan-out, and every criterion run. Only after those steps finish does it emit `task/completed`; `sandbox-cleanup-on-completed` and `sandbox-cleanup-on-failed` own teardown from terminal task events. `run-cleanup` remains a run-level reconciliation leg for residual sandbox ids recorded on the `RunRecord.summary_json`. See `03_providers.md` §4 for the definitive treatment.
7. **Sandbox lifecycle is per-task, teardown happens after evaluators.** `task-execute` invokes `sandbox-setup`, then keeps that sandbox id alive through worker execution, output persistence, evaluator fan-out, and every criterion run. Only after those steps finish does it emit `task/completed`; `sandbox-cleanup-on-completed` and `sandbox-cleanup-on-failed` own teardown from terminal task events. `sample-cleanup` remains a sample-level reconciliation leg for residual sandbox ids recorded on the `SampleRecord.summary_json`. See `03_providers.md` §4 for the definitive treatment.

8. **Workflow finalization is replay-safe.** `workflow-complete` and `workflow-failed` re-read the current `RunRecord` and evaluation rows each invocation; repeated delivery writes the same terminal status with the same completion timestamp logic. `run-cleanup` checks the status before overwriting.
8. **Workflow finalization is replay-safe.** `workflow-complete` and `workflow-failed` re-read the current `SampleRecord` and evaluation rows each invocation; repeated delivery writes the same terminal status with the same completion timestamp logic. `sample-cleanup` checks the status before overwriting.

### 4.1 Known limits

Expand Down Expand Up @@ -164,7 +164,7 @@ A brief index of where runtime functions live. The architectural claims above st
| Propagation | `core/jobs/task/propagate/` |
| Evaluator execution | `core/jobs/task/evaluate/` |
| Cancellation cascade | `core/jobs/task/cancel_orphans/`, `core/jobs/task/cleanup_cancelled/` |
| Finalization | `core/jobs/workflow/complete/`, `core/jobs/workflow/fail/`, `core/jobs/run/cleanup/` |
| Finalization | `core/jobs/workflow/complete/`, `core/jobs/workflow/fail/`, `core/jobs/sample/cleanup/` |
| Registry | `core/infrastructure/inngest/registry.py` |
| Client + cancel matchers | `core/infrastructure/inngest/client.py` |
| Runtime event contracts | `core/application/events/runtime.py` |
Expand Down
4 changes: 2 additions & 2 deletions docs/integration-spec/3-structural-checks.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,9 @@ EXPECTED_CALL_GRAPH: dict[str, dict[str, list[str]]] = {
"publishers": ["external:api"],
"subscribers": ["cancel-run"],
},
"run/cleanup": {
"sample/cleanup": {
"publishers": ["complete-workflow", "fail-workflow"],
"subscribers": ["cleanup-run"],
"subscribers": ["sample-cleanup"],
},
# criterion/evaluate: publishers unknown, subscribers MISSING — this is the live bug
# this entry must be completed and a handler added before this test can pass
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { NextResponse } from "next/server";

import { emitHarnessRunCompleted } from "@/lib/testing/dashboardHarness";
import { emitHarnessSampleCompleted } from "@/lib/testing/dashboardHarness";

export async function POST(request: Request) {
const payload = (await request.json()) as {
Expand All @@ -10,6 +10,6 @@ export async function POST(request: Request) {
finalScore: number | null;
error: string | null;
};
emitHarnessRunCompleted(payload);
emitHarnessSampleCompleted(payload);
return NextResponse.json({ ok: true });
}
12 changes: 6 additions & 6 deletions ergon-dashboard/src/app/api/health/health.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,16 @@ interface HealthResult {
}

async function runHealthChecks(deps: {
importSSRModules: () => Promise<{ parseRunSnapshot: unknown; TaskStatus: unknown }>;
importSSRModules: () => Promise<{ parseSampleSnapshot: unknown; TaskStatus: unknown }>;
fetchErgonApi: (path: string) => Promise<{ ok: boolean; status: number }>;
}): Promise<HealthResult> {
const checks: Record<string, "ok" | "fail"> = {};
const errors: string[] = [];

try {
const { parseRunSnapshot, TaskStatus } = await deps.importSSRModules();
const { parseSampleSnapshot, TaskStatus } = await deps.importSSRModules();
checks.ssr_imports =
typeof parseRunSnapshot === "function" && typeof TaskStatus !== "undefined"
typeof parseSampleSnapshot === "function" && typeof TaskStatus !== "undefined"
? "ok"
: "fail";
} catch (e) {
Expand All @@ -51,7 +51,7 @@ async function runHealthChecks(deps: {

describe("Health check logic", () => {
const okImport = async () => ({
parseRunSnapshot: () => {},
parseSampleSnapshot: () => {},
TaskStatus: { COMPLETED: "completed" },
});

Expand Down Expand Up @@ -127,10 +127,10 @@ describe("Health check logic", () => {
assert.equal(result.errors.length, 2);
});

it("returns fail for ssr_imports when parseRunSnapshot is not a function", async () => {
it("returns fail for ssr_imports when parseSampleSnapshot is not a function", async () => {
const result = await runHealthChecks({
importSSRModules: async () => ({
parseRunSnapshot: "not-a-function",
parseSampleSnapshot: "not-a-function",
TaskStatus: { COMPLETED: "completed" },
}),
fetchErgonApi: okApi,
Expand Down
2 changes: 1 addition & 1 deletion ergon-dashboard/src/app/api/health/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export async function GET() {
const rest = await import("@/lib/contracts/rest");
const types = await import("@/lib/types");
checks.ssr_imports =
typeof rest.parseRunSnapshot === "function" && typeof types.TaskStatus !== "undefined"
typeof rest.parseSampleSnapshot === "function" && typeof types.TaskStatus !== "undefined"
? "ok"
: "fail";
} catch (e) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { NextResponse } from "next/server";

import { config } from "@/lib/config";
import { fetchErgonApi } from "@/lib/serverApi";
import { getHarnessRunMutations } from "@/lib/testing/dashboardHarness";

interface RouteContext {
params: Promise<{
Expand All @@ -14,22 +12,13 @@ export async function GET(_request: Request, context: RouteContext) {
const { sampleId } = await context.params;

try {
if (config.enableTestHarness) {
const harnessMutations = getHarnessRunMutations(sampleId);
if (harnessMutations) {
return NextResponse.json(harnessMutations);
}
}
const response = await fetchErgonApi(`/samples/${sampleId}/mutations`);
const response = await fetchErgonApi(`/samples/${sampleId}/events`);
const body = await response.json();
if (response.ok) {
return NextResponse.json(body, { status: response.status });
}
return NextResponse.json(body, { status: response.status });
} catch (error) {
return NextResponse.json(
{
detail: `Ergon API is unavailable while loading mutations for run ${sampleId}.`,
detail: `Ergon API is unavailable while loading runtime events for sample ${sampleId}.`,
error: error instanceof Error ? error.message : "Unknown backend fetch failure",
},
{ status: 503 },
Expand Down
4 changes: 2 additions & 2 deletions ergon-dashboard/src/app/api/samples/[sampleId]/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { NextResponse } from "next/server";

import { loadRunSnapshot } from "@/lib/server-data/samples";
import { loadSampleSnapshot } from "@/lib/server-data/samples";

interface RouteContext {
params: Promise<{
Expand All @@ -10,7 +10,7 @@ interface RouteContext {

export async function GET(_request: Request, context: RouteContext) {
const { sampleId } = await context.params;
const result = await loadRunSnapshot(sampleId);
const result = await loadSampleSnapshot(sampleId);

if (result.ok) {
return NextResponse.json(result.data, { status: result.status });
Expand Down
4 changes: 2 additions & 2 deletions ergon-dashboard/src/app/samples/[sampleId]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { SampleWorkspacePage } from "@/components/sample/SampleWorkspacePage";
import { loadRunSnapshot } from "@/lib/server-data/samples";
import { loadSampleSnapshot } from "@/lib/server-data/samples";
import type { SerializedSampleWorkspaceState } from "@/lib/types";

interface LegacyRunPageProps {
Expand All @@ -13,7 +13,7 @@ export default async function RunPage({ params }: LegacyRunPageProps) {
let initialRunState: SerializedSampleWorkspaceState | null = null;
let ssrError: string | null = null;

const result = await loadRunSnapshot(sampleId);
const result = await loadSampleSnapshot(sampleId);
if (result.ok) {
initialRunState = result.data;
} else {
Expand Down
6 changes: 3 additions & 3 deletions ergon-dashboard/src/app/samples/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { SampleIndexTable } from "@/components/indexes/SampleIndexTable";
import { loadRunList, type RunSummary } from "@/lib/server-data/samples";
import { loadSampleList, type SampleSummary } from "@/lib/server-data/samples";

export default async function RunsPage() {
let runs: RunSummary[] = [];
let runs: SampleSummary[] = [];
let error: string | null = null;

const result = await loadRunList({ limit: 100 });
const result = await loadSampleList({ limit: 100 });
if (result.ok) {
runs = result.data;
} else {
Expand Down
4 changes: 2 additions & 2 deletions ergon-dashboard/src/components/common/StatusBadge.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"use client";

import { RunLifecycleStatus, TaskStatus } from "@/lib/types";
import { SampleLifecycleStatus, TaskStatus } from "@/lib/types";

type StatusType = TaskStatus | RunLifecycleStatus | string;
type StatusType = TaskStatus | SampleLifecycleStatus | string;

interface StatusConfig {
label: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ import {
} from "./sampleRunMetricExplorerModel";

const definitionId = "11111111-1111-4111-8111-111111111111";
const defaultRunId = "22222222-2222-4222-8222-222222222222";
const defaultSampleId = "22222222-2222-4222-8222-222222222222";

function metrics(overrides: Partial<ExperimentRunRow["metrics"]> = {}): ExperimentRunRow["metrics"] {
return {
sample_id: defaultRunId,
sample_id: defaultSampleId,
status: "completed",
instance_key: "sample-a",
tool_call_count: 0,
Expand All @@ -27,7 +27,7 @@ function metrics(overrides: Partial<ExperimentRunRow["metrics"]> = {}): Experime

function runRow(overrides: Partial<ExperimentRunRow> = {}): ExperimentRunRow {
return {
sample_id: defaultRunId,
sample_id: defaultSampleId,
definition_id: definitionId,
benchmark_type: "minif2f",
instance_key: "sample-a",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ExperimentRunRow, RunLifecycleStatus } from "@/lib/contracts/rest";
import type { ExperimentRunRow, SampleLifecycleStatus } from "@/lib/contracts/rest";
import { formatDurationMs } from "@/lib/formatDuration";

export type RunMetricKey =
Expand Down Expand Up @@ -27,7 +27,7 @@ export interface RunMetricValue {
export interface RunMetricPoint {
sampleId: string;
runName: string;
status: RunLifecycleStatus | string;
status: SampleLifecycleStatus | string;
sampleLabel: string;
instanceKey: string;
modelTarget: string | null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { useMemo, useState } from "react";
import { StatusBadge } from "@/components/common/StatusBadge";
import { formatDateTime, formatDurationSeconds, formatPercent } from "@/components/indexes/format";
import type { ExperimentSummary } from "@/lib/server-data/experiments";
import type { RunLifecycleStatus } from "@/lib/types";
import type { SampleLifecycleStatus } from "@/lib/types";

export function ExperimentIndexTable({ experiments }: { experiments: ExperimentSummary[] }) {
const [query, setQuery] = useState("");
Expand Down Expand Up @@ -97,7 +97,7 @@ export function ExperimentIndexTable({ experiments }: { experiments: ExperimentS
{experiment.failure_count}
</td>
<td className="px-3 py-2.5">
<StatusBadge status={experiment.status as RunLifecycleStatus} size="sm" />
<StatusBadge status={experiment.status as SampleLifecycleStatus} size="sm" />
</td>
<td className="px-3 py-2.5 text-right font-mono text-[var(--ink)]">
{formatPercent(experiment.average_score)}
Expand Down
8 changes: 4 additions & 4 deletions ergon-dashboard/src/components/indexes/SampleIndexTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ import {
formatNumber,
formatPercent,
} from "@/components/indexes/format";
import type { RunSummary } from "@/lib/server-data/samples";
import type { RunLifecycleStatus } from "@/lib/types";
import type { SampleSummary } from "@/lib/server-data/samples";
import type { SampleLifecycleStatus } from "@/lib/types";

export function SampleIndexTable({ runs }: { runs: RunSummary[] }) {
export function SampleIndexTable({ runs }: { runs: SampleSummary[] }) {
const [query, setQuery] = useState("");
const [status, setStatus] = useState("all");

Expand Down Expand Up @@ -110,7 +110,7 @@ export function SampleIndexTable({ runs }: { runs: RunSummary[] }) {
<div className="mt-0.5 text-[var(--faint)]">{run.sample_label}</div>
</td>
<td className="px-3 py-2.5">
<StatusBadge status={run.status as RunLifecycleStatus} size="sm" />
<StatusBadge status={run.status as SampleLifecycleStatus} size="sm" />
{run.error_message ? (
<div className="mt-1 max-w-[160px] truncate text-xs text-[var(--status-failed)]">
{run.error_message}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
type MetricDisplay,
} from "@/lib/sample-state/formatters";

export interface RunHeaderMetricValues {
export interface SampleHeaderMetricValues {
tasks: {
completed: number;
running: number;
Expand Down Expand Up @@ -59,7 +59,7 @@ function MetricTile({
);
}

export function SampleRuntimeSummaryHeader({ metrics }: { metrics: RunHeaderMetricValues }) {
export function SampleRuntimeSummaryHeader({ metrics }: { metrics: SampleHeaderMetricValues }) {
return (
<div className="hidden items-stretch gap-2 border-r border-[var(--line)] pr-3 xl:flex">
<MetricTile
Expand Down
Loading
Loading