From 54bc67017ee94df423e909ecb1af8f9eec0b51d9 Mon Sep 17 00:00:00 2001 From: "fabro-sh-0530[bot]" <281434857+fabro-sh-0530[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 21:06:40 -0400 Subject: [PATCH] feat: Replace duration/elapsed fields with wall_time_ms and StageTiming (#343) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Replaces the ambiguous `runtime_secs`, `elapsed_secs`, and `duration_ms` timing fields on run/stage public API surfaces with explicit `wall_time_ms` (elapsed clock time) and a `StageTiming` value object that also carries `inference_time_ms`, `tool_time_ms`, and `active_time_ms`. This is a greenfield breaking change — no compatibility shims are preserved. ### What changed **API shape** - `RunBillingStage.runtime_secs` → `RunBillingStage.timing: StageTiming` - `RunBillingTotals.runtime_secs` → `RunBillingTotals.timing: StageTiming` - `RunSummary.timestamps.duration_ms` / `elapsed_secs` removed; a top-level `timing: StageTiming | null` field added - Stage list item `duration_secs` → `wall_time_ms` **Web app (`apps/fabro-web`)** - `run-billing.tsx`: `liveRuntimeSecs` → `liveWallTimeMs`; live ticking now returns milliseconds and the footer total sums `wallTimeMs` across rows - `stage-sidebar.ts`: `duration_secs` → `wall_time_ms` for the per-stage duration display - `runs.ts`: `elapsed_secs` lookup replaced with `timing.wall_time_ms` - `formatElapsedSecs` / `formatDurationSecs` call sites replaced with `formatDurationMs` **Lockfile / tooling** - `@openapitools/openapi-generator-cli@2.20.2` added as a dev dependency to `@qltysh/fabro-api-client` to support regenerating the TypeScript client after schema edits; several transitive deps pulled in alongside it. ### Design notes - **Units are now consistent**: every timing value on run/stage surfaces is in milliseconds; the old API mixed seconds (`runtime_secs`, `elapsed_secs`) with milliseconds (`duration_ms`). - **Live ticking** still works correctly: the in-flight billing row computes `now - startedAt` in ms and sums across rows for the footer, avoiding a server round-trip during a running stage. - **`StageTiming.active_time_ms = inference_time_ms + tool_time_ms`** — parallel work is summed, so run active time can exceed wall time. - Subsystem-internal `duration_ms` fields (sandbox setup, devcontainer lifecycle, hooks) are intentionally left unchanged; only public run/stage timing surfaces are affected. ### Fabro Details
Ran 9 stages in 115m 53s for $108.50 | Stage | Duration | Cost | Retries | |---|---|---|---| | start | 0s | – | 0 | | toolchain | 1s | – | 0 | | preflight_compile | 2m 6s | – | 0 | | preflight_lint | 2m 18s | – | 0 | | implement | 80m 53s | $101.97 | 0 | | simplify_opus | 21m 35s | $4.09 | 0 | | simplify_gpt | 5m 1s | $2.44 | 0 | | verify | 3m 11s | – | 0 | | fmt | 3s | – | 0 | | **Total** | **115m 53s** | **$108.50** | **0** |
Ran ImplementPlan.fabro (12 nodes and 15 edges) ```dot digraph ImplementPlan { graph [ goal="Implement and simplify", model_stylesheet=" * { model: claude-opus-4-7; } " ] rankdir=LR start [shape=Mdiamond, label="Start"] exit [shape=Msquare, label="Exit"] toolchain [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0] preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0] preflight_lint [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0] fix_lints [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3] implement [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD."] simplify_opus [label="Simplify (Opus)", prompt="@prompts/simplify.md"] simplify_gpt [label="Simplify (GPT-55)", prompt="@prompts/simplify.md", model="gpt-55"] verify [label="Verify", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1 && cargo dev docs refresh 2>&1 && cargo dev docs check 2>&1", goal_gate=true, retry_target="fixup"] fixup [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all clippy lint warnings, test failures, and generated docs errors.", max_visits=3] fmt [label="Format", shape=parallelogram, script="cargo +nightly-2026-04-14 fmt --all 2>&1", max_retries=0] start -> toolchain toolchain -> preflight_compile [condition="outcome=succeeded"] toolchain -> exit preflight_compile -> preflight_lint [condition="outcome=succeeded"] preflight_compile -> exit preflight_lint -> implement [condition="outcome=succeeded"] preflight_lint -> fix_lints fix_lints -> preflight_lint implement -> simplify_opus -> simplify_gpt -> verify verify -> fmt [condition="outcome=succeeded"] verify -> fixup fixup -> verify fmt -> exit } ```
⚒️ Generated with [Fabro](https://fabro.sh) --------- Co-authored-by: Fabro Co-authored-by: Bryan Helmkamp --- apps/fabro-web/app/data/runs.test.ts | 13 +- apps/fabro-web/app/data/runs.ts | 6 +- apps/fabro-web/app/lib/stage-sidebar.test.ts | 4 +- apps/fabro-web/app/lib/stage-sidebar.ts | 8 +- .../fabro-web/app/routes/run-billing.test.tsx | 35 ++- apps/fabro-web/app/routes/run-billing.tsx | 28 +- apps/fabro-web/app/routes/run-detail.test.ts | 5 +- bun.lock | 267 +++++++++++++++++- docs/public/api-reference/fabro-api.yaml | 176 ++++++++---- lib/crates/fabro-api/build.rs | 2 + .../tests/run_billing_stage_round_trip.rs | 6 +- .../fabro-api/tests/run_failure_round_trip.rs | 9 +- .../fabro-api/tests/run_summary_round_trip.rs | 18 +- .../tests/stage_projection_round_trip.rs | 7 +- .../fabro-cli/src/commands/run/events.rs | 19 +- .../fabro-cli/src/commands/run/output.rs | 2 +- .../src/commands/run/run_progress/event.rs | 18 +- .../src/commands/run/run_progress/mod.rs | 6 +- .../fabro-cli/src/commands/run/runner.rs | 6 +- lib/crates/fabro-cli/src/commands/run/wait.rs | 17 +- .../fabro-cli/src/commands/runs/list.rs | 4 +- lib/crates/fabro-cli/src/server_runs.rs | 4 +- lib/crates/fabro-cli/tests/it/cmd/attach.rs | 9 +- lib/crates/fabro-cli/tests/it/cmd/inspect.rs | 6 +- lib/crates/fabro-cli/tests/it/cmd/run.rs | 4 +- lib/crates/fabro-cli/tests/it/cmd/support.rs | 19 +- lib/crates/fabro-cli/tests/it/cmd/wait.rs | 23 +- .../fabro-cli/tests/it/scenario/smoke.rs | 2 +- .../fabro-cli/tests/it/workflow/full_stack.rs | 4 +- lib/crates/fabro-core/src/executor.rs | 142 +++++++++- lib/crates/fabro-core/src/lifecycle.rs | 27 +- lib/crates/fabro-core/src/outcome.rs | 8 +- lib/crates/fabro-core/src/state.rs | 27 +- lib/crates/fabro-dump/src/lib.rs | 2 +- lib/crates/fabro-server/src/demo/mod.rs | 66 ++--- lib/crates/fabro-server/src/run_files.rs | 2 +- lib/crates/fabro-server/src/server.rs | 50 ++-- .../src/server/handler/billing.rs | 60 ++-- .../fabro-server/src/server/handler/system.rs | 2 +- lib/crates/fabro-server/src/server/tests.rs | 190 +++++++------ .../fabro-server/tests/it/api/run_files.rs | 2 +- .../fabro-server/tests/it/scenario/usage.rs | 20 +- lib/crates/fabro-store/src/run_state.rs | 77 +++-- lib/crates/fabro-store/src/slate/mod.rs | 8 +- .../tests/serializable_projection.rs | 4 +- lib/crates/fabro-tool/src/common.rs | 3 +- lib/crates/fabro-tool/src/create.rs | 3 +- lib/crates/fabro-tool/src/search.rs | 3 +- lib/crates/fabro-types/src/conclusion.rs | 10 +- lib/crates/fabro-types/src/event_envelope.rs | 7 +- lib/crates/fabro-types/src/lib.rs | 2 + lib/crates/fabro-types/src/outcome.rs | 45 ++- lib/crates/fabro-types/src/run_event/mod.rs | 18 +- lib/crates/fabro-types/src/run_event/run.rs | 8 +- lib/crates/fabro-types/src/run_event/stage.rs | 16 +- lib/crates/fabro-types/src/run_projection.rs | 28 +- lib/crates/fabro-types/src/run_summary.rs | 10 +- lib/crates/fabro-types/src/timing.rs | 252 +++++++++++++++++ .../fabro-types/tests/run_failure_serde.rs | 22 +- .../fabro-workflow/src/billing_rollup.rs | 59 ++-- lib/crates/fabro-workflow/src/error.rs | 16 +- .../fabro-workflow/src/event/convert.rs | 57 ++-- lib/crates/fabro-workflow/src/event/events.rs | 55 ++-- lib/crates/fabro-workflow/src/git.rs | 2 +- .../fabro-workflow/src/handler/llm/api.rs | 3 +- lib/crates/fabro-workflow/src/lib.rs | 173 ++++++++---- .../fabro-workflow/src/lifecycle/event.rs | 26 +- .../fabro-workflow/src/lifecycle/git.rs | 47 ++- .../fabro-workflow/src/operations/archive.rs | 4 +- .../fabro-workflow/src/operations/fork.rs | 2 +- .../fabro-workflow/src/operations/start.rs | 21 +- .../fabro-workflow/src/pipeline/execute.rs | 10 +- .../fabro-workflow/src/pipeline/finalize.rs | 41 +-- .../src/pipeline/pull_request.rs | 20 +- .../fabro-workflow/src/pipeline/types.rs | 3 +- lib/crates/fabro-workflow/src/run_lookup.rs | 11 +- lib/crates/fabro-workflow/src/test_support.rs | 2 +- .../fabro-workflow/tests/it/integration.rs | 2 +- lib/packages/fabro-api-client/package.json | 1 + .../src/.openapi-generator/FILES | 5 +- .../src/models/aggregate-billing-totals.ts | 7 +- .../fabro-api-client/src/models/check-run.ts | 4 +- .../fabro-api-client/src/models/conclusion.ts | 5 +- .../fabro-api-client/src/models/index.ts | 5 +- .../src/models/run-billing-stage.ts | 9 +- .../src/models/run-billing-totals.ts | 7 +- .../fabro-api-client/src/models/run-stage.ts | 4 +- .../src/models/run-timestamps.ts | 2 - .../models/{run-timings.ts => run-timing.ts} | 15 +- .../fabro-api-client/src/models/run.ts | 4 + .../src/models/stage-projection.ts | 8 +- .../src/models/stage-summary.ts | 5 +- .../src/models/stage-timing.ts | 28 ++ 93 files changed, 1788 insertions(+), 716 deletions(-) create mode 100644 lib/crates/fabro-types/src/timing.rs rename lib/packages/fabro-api-client/src/models/{run-timings.ts => run-timing.ts} (50%) create mode 100644 lib/packages/fabro-api-client/src/models/stage-timing.ts diff --git a/apps/fabro-web/app/data/runs.test.ts b/apps/fabro-web/app/data/runs.test.ts index c2365637f..8cbfdf661 100644 --- a/apps/fabro-web/app/data/runs.test.ts +++ b/apps/fabro-web/app/data/runs.test.ts @@ -36,8 +36,12 @@ function makeRun(overrides: Partial = {}): Run { started_at: "2026-04-08T12:00:00Z", last_event_at: null, completed_at: null, - duration_ms: 65000, - elapsed_secs: 65, + }, + timing: { + wall_time_ms: 65000, + inference_time_ms: 0, + tool_time_ms: 0, + active_time_ms: 0, }, billing: { total_usd_micros: 500000 }, diff: null, @@ -130,9 +134,8 @@ describe("mapRunToRunItem", () => { started_at: null, last_event_at: null, completed_at: null, - duration_ms: null, - elapsed_secs: null, }, + timing: null, billing: null, }); const item = mapRunToRunItem(summary); @@ -177,4 +180,4 @@ describe("columnForStatus", () => { test("returns null for lifecycle states that do not map to a board column", () => { expect(columnForStatus("removing")).toBeNull(); }); -}); +}); \ No newline at end of file diff --git a/apps/fabro-web/app/data/runs.ts b/apps/fabro-web/app/data/runs.ts index 36b7c2f3c..8ce8d06ed 100644 --- a/apps/fabro-web/app/data/runs.ts +++ b/apps/fabro-web/app/data/runs.ts @@ -1,4 +1,4 @@ -import { formatElapsedSecs, formatDurationSecs } from "../lib/format"; +import { formatDurationMs } from "../lib/format"; import { BoardColumn, type Run, @@ -91,7 +91,7 @@ export function mapRunListItem(item: Run): RunItem { lifecycleStatusLabel: lifecycleStatusLabel(item.lifecycle.status, item.lifecycle.archived), number: item.pull_request?.number, pullRequestUrl: item.pull_request?.html_url, - elapsed: item.timestamps.elapsed_secs != null ? formatElapsedSecs(item.timestamps.elapsed_secs) : undefined, + elapsed: item.timing != null ? formatDurationMs(item.timing.wall_time_ms) : undefined, resources: undefined, question: item.current_question?.text, sandboxId: runtime?.id ?? undefined, @@ -202,4 +202,4 @@ export const ciConfig: Record { name: "Apply Changes", handler: "command", status: "succeeded", - duration_secs: 12.5, + wall_time_ms: 12500, node_id: "apply", visit: 1, }, @@ -197,4 +197,4 @@ describe("aggregateGraphNodeStatus", () => { latestStageId: "apply@1", }); }); -}); +}); \ No newline at end of file diff --git a/apps/fabro-web/app/lib/stage-sidebar.ts b/apps/fabro-web/app/lib/stage-sidebar.ts index a9115d599..e7cd62757 100644 --- a/apps/fabro-web/app/lib/stage-sidebar.ts +++ b/apps/fabro-web/app/lib/stage-sidebar.ts @@ -3,7 +3,7 @@ import type { PaginatedRunStageList } from "@qltysh/fabro-api-client"; import type { Stage } from "../components/stage-sidebar"; import { isVisibleStage } from "../data/runs"; -import { formatDurationSecs } from "./format"; +import { formatDurationMs } from "./format"; export const ACTIVE_STAGE_STATES: ReadonlySet = new Set([ StageState.RUNNING, @@ -70,8 +70,8 @@ export function mapRunStagesToSidebarStages( nodeId: stage.node_id, visit: stage.visit, status: stage.status, - duration: stage.duration_secs != null - ? formatDurationSecs(stage.duration_secs) + duration: stage.wall_time_ms != null + ? formatDurationMs(stage.wall_time_ms) : "--", startedAt: stage.started_at ?? null, })); @@ -112,4 +112,4 @@ export function aggregateGraphNodeStatus(stages: readonly Stage[]): Map< result.set(nodeId, { displayStatus: display.status, latestStageId: latestStage.id }); } return result; -} +} \ No newline at end of file diff --git a/apps/fabro-web/app/routes/run-billing.test.tsx b/apps/fabro-web/app/routes/run-billing.test.tsx index 2940ef677..99f6ece5d 100644 --- a/apps/fabro-web/app/routes/run-billing.test.tsx +++ b/apps/fabro-web/app/routes/run-billing.test.tsx @@ -1,7 +1,20 @@ import { afterEach, describe, expect, mock, test } from "bun:test"; import TestRenderer from "react-test-renderer"; -import type { BilledTokenCounts, RunBilling } from "@qltysh/fabro-api-client"; +import type { + BilledTokenCounts, + RunBilling, + StageTiming, +} from "@qltysh/fabro-api-client"; + +function stageTiming(wall_time_ms = 0, inference_time_ms = 0, tool_time_ms = 0): StageTiming { + return { + wall_time_ms, + inference_time_ms, + tool_time_ms, + active_time_ms: inference_time_ms + tool_time_ms, + }; +} let currentBilling: RunBilling | undefined; @@ -28,7 +41,7 @@ function billing(overrides: Partial = {}): RunBilling { return { stages: [], totals: { - runtime_secs: 0, + timing: stageTiming(), ...zeroBilling(), }, by_model: [], @@ -74,19 +87,19 @@ describe("RunBilling", () => { stage: { id: "start", name: "start" }, model: null, billing: zeroBilling(), - runtime_secs: 0, + timing: stageTiming(), state: "succeeded", }, { stage: { id: "command", name: "command" }, model: null, billing: zeroBilling(), - runtime_secs: 61, + timing: stageTiming(61000), state: "succeeded", }, ], totals: { - runtime_secs: 61, + timing: stageTiming(61000), ...zeroBilling(), }, }), @@ -109,7 +122,7 @@ describe("RunBilling", () => { stage: { id: "start", name: "start" }, model: null, billing: zeroBilling(), - runtime_secs: 0, + timing: stageTiming(), state: "succeeded", }, { @@ -124,12 +137,12 @@ describe("RunBilling", () => { total_tokens: 1500, total_usd_micros: 240000, }), - runtime_secs: 42, + timing: stageTiming(42000), state: "succeeded", }, ], totals: { - runtime_secs: 42, + timing: stageTiming(42000), ...zeroBilling({ input_tokens: 1200, output_tokens: 300, @@ -197,13 +210,13 @@ describe("RunBilling", () => { total_tokens: 1500, total_usd_micros: 240000, }), - runtime_secs: 0, + timing: stageTiming(), started_at: startedAt, state: "running", }, ], totals: { - runtime_secs: 0, + timing: stageTiming(), ...zeroBilling({ input_tokens: 1200, output_tokens: 300, @@ -255,4 +268,4 @@ describe("RunBilling", () => { Date.now = originalNow; } }); -}); +}); \ No newline at end of file diff --git a/apps/fabro-web/app/routes/run-billing.tsx b/apps/fabro-web/app/routes/run-billing.tsx index 715e0d6f8..1af0ebd06 100644 --- a/apps/fabro-web/app/routes/run-billing.tsx +++ b/apps/fabro-web/app/routes/run-billing.tsx @@ -3,7 +3,7 @@ import { Fragment, useMemo } from "react"; import { EmptyState } from "../components/state"; import { Tooltip } from "../components/ui"; import { - formatDurationSecs, + formatDurationMs, formatTokenCount, formatUsdMicros, } from "../lib/format"; @@ -53,24 +53,24 @@ interface MappedStageRow { outputTokens: number | null; cacheReadTokens: number | null; cacheWriteTokens: number | null; - runtimeSecs: number; + wallTimeMs: number; totalUsdMicros: number | null | undefined; inFlight: boolean; } -function liveRuntimeSecs(stage: RunBillingStage, now: number): number { +function liveWallTimeMs(stage: RunBillingStage, now: number): number { if (stage.started_at) { const startedMs = new Date(stage.started_at).getTime(); if (Number.isFinite(startedMs)) { - return Math.max(0, (now - startedMs) / 1000); + return Math.max(0, now - startedMs); } } - return stage.runtime_secs; + return stage.timing.wall_time_ms; } export const handle = { wide: true }; -function mapStageRow(stage: RunBillingStage, runtimeSecs: number): MappedStageRow { +function mapStageRow(stage: RunBillingStage, wallTimeMs: number): MappedStageRow { const hasModel = stage.model != null; return { stage: stage.stage.name, @@ -81,7 +81,7 @@ function mapStageRow(stage: RunBillingStage, runtimeSecs: number): MappedStageRo : null, cacheReadTokens: hasModel ? stage.billing.cache_read_tokens : null, cacheWriteTokens: hasModel ? stage.billing.cache_write_tokens : null, - runtimeSecs, + wallTimeMs, totalUsdMicros: stage.billing.total_usd_micros, inFlight: isInFlight(stage), }; @@ -181,7 +181,7 @@ export default function RunBilling({ params }: { params: { id: string } }) { // don't reallocate them every tick. const completedRows = useMemo(() => { if (!billing) return []; - return billing.stages.map((stage) => mapStageRow(stage, stage.runtime_secs)); + return billing.stages.map((stage) => mapStageRow(stage, stage.timing.wall_time_ms)); }, [billing]); // The model breakdown is server-derived and stable across ticks too. @@ -206,16 +206,16 @@ export default function RunBilling({ params }: { params: { id: string } }) { if (!hasInFlight) return completedRows; return billing.stages.map((stage, idx) => isInFlight(stage) - ? mapStageRow(stage, liveRuntimeSecs(stage, now)) + ? mapStageRow(stage, liveWallTimeMs(stage, now)) : completedRows[idx], ); }, [billing, completedRows, hasInFlight, now]); // While ticking, sum the displayed row runtimes so the footer updates in // lock-step. Otherwise trust the server's authoritative total. - const totalRuntimeSecs = hasInFlight - ? rows.reduce((sum, row) => sum + row.runtimeSecs, 0) - : (billing?.totals.runtime_secs ?? 0); + const totalWallTimeMs = hasInFlight + ? rows.reduce((sum, row) => sum + row.wallTimeMs, 0) + : (billing?.totals.timing.wall_time_ms ?? 0); const hasLlmStages = (billing?.by_model.length ?? 0) > 0; const totalInput = hasLlmStages ? (billing?.totals.input_tokens ?? null) : null; @@ -276,7 +276,7 @@ export default function RunBilling({ params }: { params: { id: string } }) { /> - {formatDurationSecs(row.runtimeSecs)} + {formatDurationMs(row.wallTimeMs)} {formatUsdMicrosOrDash(row.totalUsdMicros)} @@ -297,7 +297,7 @@ export default function RunBilling({ params }: { params: { id: string } }) { /> - {formatDurationSecs(totalRuntimeSecs)} + {formatDurationMs(totalWallTimeMs)} {formatUsdMicrosOrDash(totalUsdMicros)} diff --git a/apps/fabro-web/app/routes/run-detail.test.ts b/apps/fabro-web/app/routes/run-detail.test.ts index 6a6f72d8f..84a012e8a 100644 --- a/apps/fabro-web/app/routes/run-detail.test.ts +++ b/apps/fabro-web/app/routes/run-detail.test.ts @@ -104,9 +104,8 @@ function makeRunSummary( started_at: null, last_event_at: null, completed_at: null, - duration_ms: null, - elapsed_secs: null, }, + timing: null, billing: null, diff: diffSummary, pull_request: pullRequest, @@ -548,4 +547,4 @@ describe("RunDetail full-height child routes", () => { ); expect(outletWrappers).toHaveLength(1); }); -}); +}); \ No newline at end of file diff --git a/bun.lock b/bun.lock index dd8729a4f..48c749309 100644 --- a/bun.lock +++ b/bun.lock @@ -77,6 +77,7 @@ "axios": "^1.7.0", }, "devDependencies": { + "@openapitools/openapi-generator-cli": "2.20.2", "typescript": "^5.9.2", }, }, @@ -146,6 +147,8 @@ "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + "@borewit/text-codec": ["@borewit/text-codec@0.2.2", "", {}, "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ=="], + "@capsizecss/unpack": ["@capsizecss/unpack@4.0.0", "", { "dependencies": { "fontkitten": "^1.0.0" } }, "sha512-VERIM64vtTP1C4mxQ5thVT9fK0apjPFobqybMtA1UdUujWka24ERHbRHFGmpbbhp73MhV+KSsHQH9C6uOTdEQA=="], "@clack/core": ["@clack/core@1.2.0", "", { "dependencies": { "fast-wrap-ansi": "^0.1.3", "sisteransi": "^1.0.5" } }, "sha512-qfxof/3T3t9DPU/Rj3OmcFyZInceqj/NVtO9rwIuJqCUgh32gwPjpFQQp/ben07qKlhpwq7GzfWpST4qdJ5Drg=="], @@ -294,6 +297,8 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@lukeed/csprng": ["@lukeed/csprng@1.1.0", "", {}, "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA=="], + "@mediabunny/aac-encoder": ["@mediabunny/aac-encoder@1.39.2", "", { "peerDependencies": { "mediabunny": "^1.0.0" } }, "sha512-KD6KADVzAnW7tqhRFGBOX4uaiHbd0Yxvg0lfthj3wJLAEEgEBAvi43w+ZXWeEn54X/jpabrLe4bW/eYFFvlbUA=="], "@mediabunny/flac-encoder": ["@mediabunny/flac-encoder@1.39.2", "", { "peerDependencies": { "mediabunny": "^1.0.0" } }, "sha512-VwBr3AzZTPEEPvt4aladZiXwOf3W293eq213zDupGQi/taS8WWNqDd3eBdf8FfvlbXATfbRiycXDKyQ0HlOZaQ=="], @@ -314,6 +319,18 @@ "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.0.7", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@tybys/wasm-util": "^0.10.1" } }, "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw=="], + "@nestjs/axios": ["@nestjs/axios@4.0.0", "", { "peerDependencies": { "@nestjs/common": "^10.0.0 || ^11.0.0", "axios": "^1.3.1", "rxjs": "^7.0.0" } }, "sha512-1cB+Jyltu/uUPNQrpUimRHEQHrnQrpLzVj6dU3dgn6iDDDdahr10TgHFGTmw5VuJ9GzKZsCLDL78VSwJAs/9JQ=="], + + "@nestjs/common": ["@nestjs/common@11.1.1", "", { "dependencies": { "file-type": "20.5.0", "iterare": "1.2.1", "load-esm": "1.0.2", "tslib": "2.8.1", "uid": "2.0.2" }, "peerDependencies": { "class-transformer": ">=0.4.1", "class-validator": ">=0.13.2", "reflect-metadata": "^0.1.12 || ^0.2.0", "rxjs": "^7.1.0" }, "optionalPeers": ["class-transformer", "class-validator"] }, "sha512-crzp+1qeZ5EGL0nFTPy9NrVMAaUWewV5AwtQyv6SQ9yQPXwRl9W9hm1pt0nAtUu5QbYMbSuo7lYcF81EjM+nCA=="], + + "@nestjs/core": ["@nestjs/core@11.1.1", "", { "dependencies": { "@nuxt/opencollective": "0.4.1", "fast-safe-stringify": "2.1.1", "iterare": "1.2.1", "path-to-regexp": "8.2.0", "tslib": "2.8.1", "uid": "2.0.2" }, "peerDependencies": { "@nestjs/common": "^11.0.0", "@nestjs/microservices": "^11.0.0", "@nestjs/platform-express": "^11.0.0", "@nestjs/websockets": "^11.0.0", "reflect-metadata": "^0.1.12 || ^0.2.0", "rxjs": "^7.1.0" }, "optionalPeers": ["@nestjs/microservices", "@nestjs/platform-express", "@nestjs/websockets"] }, "sha512-UFoUAgLKFT+RwHTANJdr0dF7p0qS9QjkaUPjg8aafnjM/qxxxrUVDB49nVvyMlk+Hr1+vvcNaOHbWWQBxoZcHA=="], + + "@nuxt/opencollective": ["@nuxt/opencollective@0.4.1", "", { "dependencies": { "consola": "^3.2.3" }, "bin": { "opencollective": "bin/opencollective.js" } }, "sha512-GXD3wy50qYbxCJ652bDrDzgMr3NFEkIS374+IgFQKkCvk9yiYcLvX2XDYr7UyQxf4wK0e+yqDYRubZ0DtOxnmQ=="], + + "@nuxtjs/opencollective": ["@nuxtjs/opencollective@0.3.2", "", { "dependencies": { "chalk": "^4.1.0", "consola": "^2.15.0", "node-fetch": "^2.6.1" }, "bin": { "opencollective": "bin/opencollective.js" } }, "sha512-um0xL3fO7Mf4fDxcqx9KryrB7zgRM5JSlvGN5AGkP6JLM5XEKyjeAiPbNxdXVXQ16isuAhYpvP88NgL2BGd6aA=="], + + "@openapitools/openapi-generator-cli": ["@openapitools/openapi-generator-cli@2.20.2", "", { "dependencies": { "@nestjs/axios": "4.0.0", "@nestjs/common": "11.1.1", "@nestjs/core": "11.1.1", "@nuxtjs/opencollective": "0.3.2", "axios": "1.9.0", "chalk": "4.1.2", "commander": "8.3.0", "compare-versions": "4.1.4", "concurrently": "6.5.1", "console.table": "0.10.0", "fs-extra": "11.3.0", "glob": "9.3.5", "inquirer": "8.2.6", "lodash": "4.17.21", "proxy-agent": "6.5.0", "reflect-metadata": "0.2.2", "rxjs": "7.8.2", "tslib": "2.8.1" }, "bin": { "openapi-generator-cli": "main.js" } }, "sha512-dNFwQcQu6+rmEWSJj4KUx468+p6Co7nfpVgi5QEfVhzKj7wBytz9GEhCN2qmVgtg3ZX8H6nxbXI8cjh7hAxAqg=="], + "@oslojs/encoding": ["@oslojs/encoding@1.1.0", "", {}, "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ=="], "@parcel/watcher": ["@parcel/watcher@2.5.6", "", { "dependencies": { "detect-libc": "^2.0.3", "is-glob": "^4.0.3", "node-addon-api": "^7.0.0", "picomatch": "^4.0.3" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.6", "@parcel/watcher-darwin-arm64": "2.5.6", "@parcel/watcher-darwin-x64": "2.5.6", "@parcel/watcher-freebsd-x64": "2.5.6", "@parcel/watcher-linux-arm-glibc": "2.5.6", "@parcel/watcher-linux-arm-musl": "2.5.6", "@parcel/watcher-linux-arm64-glibc": "2.5.6", "@parcel/watcher-linux-arm64-musl": "2.5.6", "@parcel/watcher-linux-x64-glibc": "2.5.6", "@parcel/watcher-linux-x64-musl": "2.5.6", "@parcel/watcher-win32-arm64": "2.5.6", "@parcel/watcher-win32-ia32": "2.5.6", "@parcel/watcher-win32-x64": "2.5.6" } }, "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ=="], @@ -668,6 +685,12 @@ "@tanstack/virtual-core": ["@tanstack/virtual-core@3.13.19", "", {}, "sha512-/BMP7kNhzKOd7wnDeB8NrIRNLwkf5AhCYCvtfZV2GXWbBieFm/el0n6LOAXlTi6ZwHICSNnQcIxRCWHrLzDY+g=="], + "@tokenizer/inflate": ["@tokenizer/inflate@0.2.7", "", { "dependencies": { "debug": "^4.4.0", "fflate": "^0.8.2", "token-types": "^6.0.0" } }, "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg=="], + + "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], + + "@tootallnate/quickjs-emscripten": ["@tootallnate/quickjs-emscripten@0.23.0", "", {}, "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA=="], + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], @@ -760,12 +783,20 @@ "acorn-import-phases": ["acorn-import-phases@1.0.4", "", { "peerDependencies": { "acorn": "^8.14.0" } }, "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ=="], + "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + "ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="], "ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="], "ajv-keywords": ["ajv-keywords@3.5.2", "", { "peerDependencies": { "ajv": "^6.9.1" } }, "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ=="], + "ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], + + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], @@ -792,14 +823,26 @@ "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.0", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA=="], + "basic-ftp": ["basic-ftp@5.3.1", "", {}, "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw=="], + "big.js": ["big.js@5.2.2", "", {}, "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ=="], + "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], + "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], + "brace-expansion": ["brace-expansion@2.1.0", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="], + "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], + "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], + "buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="], "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], @@ -810,6 +853,8 @@ "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="], @@ -818,6 +863,8 @@ "character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="], + "chardet": ["chardet@0.7.0", "", {}, "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA=="], + "chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], "chrome-trace-event": ["chrome-trace-event@1.0.4", "", {}, "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ=="], @@ -828,16 +875,38 @@ "classnames": ["classnames@2.5.1", "", {}, "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="], + "cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="], + + "cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], + + "cli-width": ["cli-width@3.0.0", "", {}, "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw=="], + + "cliui": ["cliui@7.0.4", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^7.0.0" } }, "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ=="], + + "clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="], + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], - "commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], + "commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], "common-ancestor-path": ["common-ancestor-path@2.0.0", "", {}, "sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng=="], + "compare-versions": ["compare-versions@4.1.4", "", {}, "sha512-FemMreK9xNyL8gQevsdRMrvO4lFCkQP7qbuktn1q8ndcNk1+0mz7lgE7b/sNvbhVgY4w6tMN1FDp6aADjqw2rw=="], + + "concurrently": ["concurrently@6.5.1", "", { "dependencies": { "chalk": "^4.1.0", "date-fns": "^2.16.1", "lodash": "^4.17.21", "rxjs": "^6.6.3", "spawn-command": "^0.0.2-1", "supports-color": "^8.1.0", "tree-kill": "^1.2.2", "yargs": "^16.2.0" }, "bin": { "concurrently": "bin/concurrently.js" } }, "sha512-FlSwNpGjWQfRwPLXvJ/OgysbBxPkWpiVjy1042b0U7on7S7qwwMIILRj7WTN1mTgqa582bG6NFuScOoh6Zgdag=="], + + "consola": ["consola@2.15.3", "", {}, "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw=="], + + "console.table": ["console.table@0.10.0", "", { "dependencies": { "easy-table": "1.1.0" } }, "sha512-dPyZofqggxuvSf7WXvNjuRfnsOk1YazkVP8FdxH4tcH2c37wc79/Yl6Bhr7Lsu00KMgy2ql/qCMuNu8xctZM8g=="], + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], @@ -862,14 +931,22 @@ "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + "data-uri-to-buffer": ["data-uri-to-buffer@6.0.2", "", {}, "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw=="], + + "date-fns": ["date-fns@2.30.0", "", { "dependencies": { "@babel/runtime": "^7.21.0" } }, "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="], + "defaults": ["defaults@1.0.4", "", { "dependencies": { "clone": "^1.0.2" } }, "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A=="], + "define-lazy-prop": ["define-lazy-prop@2.0.0", "", {}, "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="], "defu": ["defu@6.1.7", "", {}, "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ=="], + "degenerator": ["degenerator@5.0.1", "", { "dependencies": { "ast-types": "^0.13.4", "escodegen": "^2.1.0", "esprima": "^4.0.1" } }, "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ=="], + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], @@ -902,8 +979,12 @@ "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + "easy-table": ["easy-table@1.1.0", "", { "optionalDependencies": { "wcwidth": ">=1.0.1" } }, "sha512-oq33hWOSSnl2Hoh00tZWaIPi1ievrD9aFG82/IgjlycAnW9hHx5PkJiXpxPsgEE+H7BsbVQXFVFST8TEXS6/pA=="], + "electron-to-chromium": ["electron-to-chromium@1.5.302", "", {}, "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg=="], + "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + "emojis-list": ["emojis-list@3.0.0", "", {}, "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q=="], "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], @@ -928,7 +1009,9 @@ "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], - "escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], + "escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], + + "escodegen": ["escodegen@2.1.0", "", { "dependencies": { "esprima": "^4.0.1", "estraverse": "^5.2.0", "esutils": "^2.0.2" }, "optionalDependencies": { "source-map": "~0.6.1" }, "bin": { "esgenerate": "bin/esgenerate.js", "escodegen": "bin/escodegen.js" } }, "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w=="], "eslint-scope": ["eslint-scope@5.1.1", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw=="], @@ -942,6 +1025,8 @@ "estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + "eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="], @@ -950,6 +1035,8 @@ "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], + "external-editor": ["external-editor@3.1.0", "", { "dependencies": { "chardet": "^0.7.0", "iconv-lite": "^0.4.24", "tmp": "^0.0.33" } }, "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew=="], + "extract-zip": ["extract-zip@2.0.1", "", { "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", "yauzl": "^2.10.0" }, "optionalDependencies": { "@types/yauzl": "^2.9.1" }, "bin": { "extract-zip": "cli.js" } }, "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg=="], "fabro-remotion": ["fabro-remotion@workspace:apps/remotion"], @@ -960,6 +1047,8 @@ "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + "fast-safe-stringify": ["fast-safe-stringify@2.1.1", "", {}, "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="], + "fast-string-truncated-width": ["fast-string-truncated-width@1.2.1", "", {}, "sha512-Q9acT/+Uu3GwGj+5w/zsGuQjh9O1TyywhIwAxHudtWrgF09nHOPrvTLhQevPbttcxjr/SNN7mJmfOw/B1bXgow=="], "fast-string-width": ["fast-string-width@1.1.0", "", { "dependencies": { "fast-string-truncated-width": "^1.2.0" } }, "sha512-O3fwIVIH5gKB38QNbdg+3760ZmGz0SZMgvwJbA1b2TGXceKE6A2cOlfogh1iw8lr049zPyd7YADHy+B7U4W9bQ=="], @@ -972,6 +1061,12 @@ "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + "fflate": ["fflate@0.8.3", "", {}, "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA=="], + + "figures": ["figures@3.2.0", "", { "dependencies": { "escape-string-regexp": "^1.0.5" } }, "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg=="], + + "file-type": ["file-type@20.5.0", "", { "dependencies": { "@tokenizer/inflate": "^0.2.6", "strtok3": "^10.2.0", "token-types": "^6.0.0", "uint8array-extras": "^1.4.0" } }, "sha512-BfHZtG/l9iMm4Ecianu7P8HRD2tBHLtjXinm4X62XBOYzi7CYA7jyqfJzOvXHqzVrVPYqBo2/GvbARMaaJkKVg=="], + "flattie": ["flattie@1.1.1", "", {}, "sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ=="], "follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="], @@ -982,14 +1077,20 @@ "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], + "fs-extra": ["fs-extra@11.3.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew=="], + "fs-monkey": ["fs-monkey@1.0.3", "", {}, "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q=="], + "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], @@ -998,8 +1099,12 @@ "get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], + "get-uri": ["get-uri@6.0.5", "", { "dependencies": { "basic-ftp": "^5.0.2", "data-uri-to-buffer": "^6.0.2", "debug": "^4.3.4" } }, "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg=="], + "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], + "glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="], + "glob-to-regexp": ["glob-to-regexp@0.4.1", "", {}, "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="], "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], @@ -1048,12 +1153,26 @@ "http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="], + "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], + + "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + "human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="], + "iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], + "icss-utils": ["icss-utils@5.1.0", "", { "peerDependencies": { "postcss": "^8.1.0" } }, "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA=="], + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="], + "inquirer": ["inquirer@8.2.6", "", { "dependencies": { "ansi-escapes": "^4.2.1", "chalk": "^4.1.1", "cli-cursor": "^3.1.0", "cli-width": "^3.0.0", "external-editor": "^3.0.3", "figures": "^3.0.0", "lodash": "^4.17.21", "mute-stream": "0.0.8", "ora": "^5.4.1", "run-async": "^2.4.0", "rxjs": "^7.5.5", "string-width": "^4.1.0", "strip-ansi": "^6.0.0", "through": "^2.3.6", "wrap-ansi": "^6.0.1" } }, "sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg=="], + + "ip-address": ["ip-address@10.2.0", "", {}, "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA=="], + "iron-webcrypto": ["iron-webcrypto@1.2.1", "", {}, "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg=="], "is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="], @@ -1066,20 +1185,28 @@ "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="], "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="], + "is-interactive": ["is-interactive@1.0.0", "", {}, "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w=="], + "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], + "is-unicode-supported": ["is-unicode-supported@0.1.0", "", {}, "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="], + "is-wsl": ["is-wsl@3.1.1", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="], "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + "iterare": ["iterare@1.2.1", "", {}, "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q=="], + "jest-worker": ["jest-worker@27.5.1", "", { "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg=="], "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], @@ -1096,6 +1223,8 @@ "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + "jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], + "kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], @@ -1122,15 +1251,21 @@ "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + "load-esm": ["load-esm@1.0.2", "", {}, "sha512-nVAvWk/jeyrWyXEAs84mpQCYccxRqgKY4OznLuJhJCa0XsPSfdOIr2zvBZEj3IHEHbX97jjscKRRV539bW0Gpw=="], + "loader-runner": ["loader-runner@4.3.1", "", {}, "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q=="], "loader-utils": ["loader-utils@2.0.4", "", { "dependencies": { "big.js": "^5.2.2", "emojis-list": "^3.0.0", "json5": "^2.1.2" } }, "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw=="], + "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + "lodash.sortby": ["lodash.sortby@4.7.0", "", {}, "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA=="], + "log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="], + "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], - "lru-cache": ["lru-cache@11.3.5", "", {}, "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw=="], + "lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], "lru_map": ["lru_map@0.4.1", "", {}, "sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg=="], @@ -1250,24 +1385,34 @@ "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], + "minimatch": ["minimatch@8.0.7", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-V+1uQNdzybxa14e/p00HZnQNNcTjnRJjDxg2V8wtkjFctq4M7hXFws4oekyTP0Jebeq7QYtpFyOeBAjc88zvYg=="], + "minimist": ["minimist@1.2.6", "", {}, "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="], + "minipass": ["minipass@4.2.8", "", {}, "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ=="], + "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="], "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "mute-stream": ["mute-stream@0.0.8", "", {}, "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA=="], + "nanoid": ["nanoid@5.1.11", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-v+KEsUv2ps74PaSKv0gHTxTCgMXOIfBEbaqa6w6ISIGC7ZsvHN4N9oJ8d4cmf0n5oTzQz2SLmThbQWhjd/8eKg=="], "neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="], "neotraverse": ["neotraverse@0.6.18", "", {}, "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA=="], + "netmask": ["netmask@2.1.1", "", {}, "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA=="], + "nlcst-to-string": ["nlcst-to-string@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0" } }, "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA=="], "node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="], + "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], + "node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="], "node-mock-http": ["node-mock-http@1.0.4", "", {}, "sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ=="], @@ -1296,12 +1441,20 @@ "open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="], + "ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="], + + "os-tmpdir": ["os-tmpdir@1.0.2", "", {}, "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g=="], + "p-limit": ["p-limit@7.3.0", "", { "dependencies": { "yocto-queue": "^1.2.1" } }, "sha512-7cIXg/Z0M5WZRblrsOla88S4wAK+zOQQWeBYfV3qJuJXMr+LnbYjaadrFaS0JILfEDPVqHyKnZ1Z/1d6J9VVUw=="], "p-queue": ["p-queue@9.1.2", "", { "dependencies": { "eventemitter3": "^5.0.1", "p-timeout": "^7.0.0" } }, "sha512-ktsDOALzTYTWWF1PbkNVg2rOt+HaOaMWJMUnt7T3qf5tvZ1L8dBW3tObzprBcXNMKkwj+yFSLqHso0x+UFcJXw=="], "p-timeout": ["p-timeout@7.0.1", "", {}, "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg=="], + "pac-proxy-agent": ["pac-proxy-agent@7.2.0", "", { "dependencies": { "@tootallnate/quickjs-emscripten": "^0.23.0", "agent-base": "^7.1.2", "debug": "^4.3.4", "get-uri": "^6.0.1", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.6", "pac-resolver": "^7.0.1", "socks-proxy-agent": "^8.0.5" } }, "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA=="], + + "pac-resolver": ["pac-resolver@7.0.1", "", { "dependencies": { "degenerator": "^5.0.0", "netmask": "^2.0.2" } }, "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg=="], + "package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="], "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="], @@ -1312,6 +1465,10 @@ "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + + "path-to-regexp": ["path-to-regexp@8.2.0", "", {}, "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ=="], + "pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="], "piccolore": ["piccolore@0.1.3", "", {}, "sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw=="], @@ -1346,6 +1503,8 @@ "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], + "proxy-agent": ["proxy-agent@6.5.0", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "http-proxy-agent": "^7.0.1", "https-proxy-agent": "^7.0.6", "lru-cache": "^7.14.1", "pac-proxy-agent": "^7.1.0", "proxy-from-env": "^1.1.0", "socks-proxy-agent": "^8.0.5" } }, "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A=="], + "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], "pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="], @@ -1378,10 +1537,14 @@ "react-textarea-autosize": ["react-textarea-autosize@8.5.9", "", { "dependencies": { "@babel/runtime": "^7.20.13", "use-composed-ref": "^1.3.0", "use-latest": "^1.2.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-U1DGlIQN5AwgjTyOEnI1oCcMuEr1pv1qOtklB2l4nyMGbHzWrI0eFsYK0zos2YWqAolJyG0IWJaqWmWj5ETh0A=="], + "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + "readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], "recast": ["recast@0.23.11", "", { "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", "source-map": "~0.6.1", "tiny-invariant": "^1.3.3", "tslib": "^2.0.1" } }, "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA=="], + "reflect-metadata": ["reflect-metadata@0.2.2", "", {}, "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q=="], + "regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="], "regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="], @@ -1408,8 +1571,12 @@ "remotion": ["remotion@4.0.437", "", { "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-mQHiYZwt3HoMngJeTGyytVFdobf/mgsPTiQSUfPP43kA7bEpn4OdaF4hWoMfJhjfTC1ZdkTIRX/s3OKto0aWzg=="], + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + "restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="], + "retext": ["retext@9.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "retext-latin": "^4.0.0", "retext-stringify": "^4.0.0", "unified": "^11.0.0" } }, "sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA=="], "retext-latin": ["retext-latin@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "parse-latin": "^7.0.0", "unified": "^11.0.0" } }, "sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA=="], @@ -1420,8 +1587,16 @@ "rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="], + "run-async": ["run-async@2.4.1", "", {}, "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ=="], + + "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + "safe-content-frame": ["safe-content-frame@0.0.19", "", {}, "sha512-+R0IHHjvghT5O8bc8itf9AoS9MvzhUcD0p+hNINLgyEuFQJug3wt3ZuhLFZFG3bUzHi8UfQED4p6J3/Ft9oCtg=="], + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + "sax": ["sax@1.5.0", "", {}, "sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA=="], "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], @@ -1446,8 +1621,14 @@ "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], + "smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="], + "smol-toml": ["smol-toml@1.6.0", "", {}, "sha512-4zemZi0HvTnYwLfrpk/CF9LOd9Lt87kAt50GnqhMpyF9U3poDAP2+iukq2bZsO/ufegbYehBkqINbsWxj4l4cw=="], + "socks": ["socks@2.8.9", "", { "dependencies": { "ip-address": "^10.1.1", "smart-buffer": "^4.2.0" } }, "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw=="], + + "socks-proxy-agent": ["socks-proxy-agent@8.0.5", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "socks": "^2.8.3" } }, "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw=="], + "source-map": ["source-map@0.7.3", "", {}, "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], @@ -1456,19 +1637,29 @@ "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], + "spawn-command": ["spawn-command@0.0.2", "", {}, "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ=="], + "stackframe": ["stackframe@1.3.4", "", {}, "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw=="], + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="], + "strtok3": ["strtok3@10.3.5", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA=="], + "style-loader": ["style-loader@4.0.0", "", { "peerDependencies": { "webpack": "^5.27.0" } }, "sha512-1V4WqhhZZgjVAVJyt7TdDPZoPBPNHbekX4fWnCJL1yQukhCeZhJySUL+gL9y6sNdN95uEOS83Y55SqHcP7MzLA=="], "style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="], "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="], - "supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], "svgo": ["svgo@4.0.1", "", { "dependencies": { "commander": "^11.1.0", "css-select": "^5.1.0", "css-tree": "^3.0.1", "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.1.1", "sax": "^1.5.0" }, "bin": "./bin/svgo.js" }, "sha512-XDpWUOPC6FEibaLzjfe0ucaV0YrOjYotGJO1WpF0Zd+n6ZGEQUsSugaoLq9QkEZtAfQIxT42UChcssDVPP3+/w=="], @@ -1484,6 +1675,8 @@ "terser-webpack-plugin": ["terser-webpack-plugin@5.4.0", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", "schema-utils": "^4.3.0", "terser": "^5.31.1" }, "peerDependencies": { "webpack": "^5.1.0" } }, "sha512-Bn5vxm48flOIfkdl5CaD2+1CiUVbonWQ3KQPyP7/EuIl9Gbzq/gQFOzaMFUEgVjB1396tcK0SG8XcNJ/2kDH8g=="], + "through": ["through@2.3.8", "", {}, "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="], + "tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="], "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], @@ -1494,7 +1687,13 @@ "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], - "tr46": ["tr46@1.0.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA=="], + "tmp": ["tmp@0.0.33", "", { "dependencies": { "os-tmpdir": "~1.0.2" } }, "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw=="], + + "token-types": ["token-types@6.1.2", "", { "dependencies": { "@borewit/text-codec": "^0.2.1", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww=="], + + "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], + + "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], @@ -1504,10 +1703,16 @@ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="], + "uid": ["uid@2.0.2", "", { "dependencies": { "@lukeed/csprng": "^1.0.0" } }, "sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g=="], + + "uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="], + "ultrahtml": ["ultrahtml@1.6.0", "", {}, "sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw=="], "uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="], @@ -1536,6 +1741,8 @@ "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="], + "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], + "unstorage": ["unstorage@1.17.5", "", { "dependencies": { "anymatch": "^3.1.3", "chokidar": "^5.0.0", "destr": "^2.0.5", "h3": "^1.15.10", "lru-cache": "^11.2.7", "node-fetch-native": "^1.6.7", "ofetch": "^1.5.1", "ufo": "^1.6.3" }, "peerDependencies": { "@azure/app-configuration": "^1.8.0", "@azure/cosmos": "^4.2.0", "@azure/data-tables": "^13.3.0", "@azure/identity": "^4.6.0", "@azure/keyvault-secrets": "^4.9.0", "@azure/storage-blob": "^12.26.0", "@capacitor/preferences": "^6 || ^7 || ^8", "@deno/kv": ">=0.9.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.34.3", "@vercel/blob": ">=0.27.1", "@vercel/functions": "^2.2.12 || ^3.0.0", "@vercel/kv": "^1 || ^2 || ^3", "aws4fetch": "^1.0.20", "db0": ">=0.2.1", "idb-keyval": "^6.2.1", "ioredis": "^5.4.2", "uploadthing": "^7.4.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/functions", "@vercel/kv", "aws4fetch", "db0", "idb-keyval", "ioredis", "uploadthing"] }, "sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg=="], "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], @@ -1570,28 +1777,36 @@ "watchpack": ["watchpack@2.5.1", "", { "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" } }, "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg=="], + "wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="], + "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="], - "webidl-conversions": ["webidl-conversions@4.0.2", "", {}, "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg=="], + "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], "webpack": ["webpack@5.105.0", "", { "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", "@types/json-schema": "^7.0.15", "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", "acorn": "^8.15.0", "acorn-import-phases": "^1.0.3", "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", "enhanced-resolve": "^5.19.0", "es-module-lexer": "^2.0.0", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", "loader-runner": "^4.3.1", "mime-types": "^2.1.27", "neo-async": "^2.6.2", "schema-utils": "^4.3.3", "tapable": "^2.3.0", "terser-webpack-plugin": "^5.3.16", "watchpack": "^2.5.1", "webpack-sources": "^3.3.3" }, "bin": { "webpack": "bin/webpack.js" } }, "sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw=="], "webpack-sources": ["webpack-sources@3.3.4", "", {}, "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q=="], - "whatwg-url": ["whatwg-url@7.1.0", "", { "dependencies": { "lodash.sortby": "^4.7.0", "tr46": "^1.0.1", "webidl-conversions": "^4.0.2" } }, "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg=="], + "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "which-pm-runs": ["which-pm-runs@1.1.0", "", {}, "sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA=="], + "wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], "ws": ["ws@8.17.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ=="], "xxhash-wasm": ["xxhash-wasm@1.1.0", "", {}, "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA=="], + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + "yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + "yargs": ["yargs@16.2.0", "", { "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.0", "y18n": "^5.0.5", "yargs-parser": "^20.2.2" } }, "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw=="], + "yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="], "yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="], @@ -1620,6 +1835,10 @@ "@babel/traverse/@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="], + "@nuxt/opencollective/consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], + + "@openapitools/openapi-generator-cli/axios": ["axios@1.9.0", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg=="], + "@parcel/watcher/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], "@radix-ui/react-accordion/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], @@ -1816,24 +2035,44 @@ "astro/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "concurrently/rxjs": ["rxjs@6.6.7", "", { "dependencies": { "tslib": "^1.9.0" } }, "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ=="], + + "concurrently/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], + "csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="], + "degenerator/ast-types": ["ast-types@0.13.4", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w=="], + "dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + "escodegen/estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + + "escodegen/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + "esrecurse/estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], "extract-zip/get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="], "is-inside-container/is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], + "jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], + "magicast/@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="], + "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], + "open/is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="], "open/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], + "path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + + "path-scurry/minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + "postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], "postcss-modules-local-by-default/postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="], @@ -1852,14 +2091,20 @@ "source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + "svgo/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], + "terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], "terser-webpack-plugin/schema-utils": ["schema-utils@4.3.3", "", { "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", "ajv-formats": "^2.1.1", "ajv-keywords": "^5.1.0" } }, "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA=="], "tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + "unstorage/lru-cache": ["lru-cache@11.3.5", "", {}, "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw=="], + "webpack/schema-utils": ["schema-utils@4.3.3", "", { "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", "ajv-formats": "^2.1.1", "ajv-keywords": "^5.1.0" } }, "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA=="], + "yargs/yargs-parser": ["yargs-parser@20.2.9", "", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="], + "@astrojs/markdown-remark/shiki/@shikijs/core": ["@shikijs/core@4.0.2", "", { "dependencies": { "@shikijs/primitive": "4.0.2", "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-hxT0YF4ExEqB8G/qFdtJvpmHXBYJ2lWW7qTHDarVkIudPFE6iCIrqdgWxGn5s+ppkGXI0aEGlibI0PAyzP3zlw=="], "@astrojs/markdown-remark/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@4.0.2", "", { "dependencies": { "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-7PW0Nm49DcoUIQEXlJhNNBHyoGMjalRETTCcjMqEaMoJRLljy1Bi/EGV3/qLBgLKQejdspiiYuHGQW6dX94Nag=="], @@ -1986,6 +2231,8 @@ "@remotion/bundler/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.0", "", { "os": "win32", "cpu": "x64" }, "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ=="], + "@remotion/renderer/source-map/whatwg-url": ["whatwg-url@7.1.0", "", { "dependencies": { "lodash.sortby": "^4.7.0", "tr46": "^1.0.1", "webidl-conversions": "^4.0.2" } }, "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg=="], + "@remotion/studio-server/semver/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], "@remotion/studio/semver/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], @@ -2030,6 +2277,8 @@ "astro/shiki/@shikijs/types": ["@shikijs/types@4.0.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-qzbeRooUTPnLE+sHD/Z8DStmaDgnbbc/pMrU203950aRqjX/6AFHeDYT+j00y2lPdz0ywJKx7o/7qnqTivtlXg=="], + "concurrently/rxjs/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], + "csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="], "terser-webpack-plugin/schema-utils/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], @@ -2040,6 +2289,10 @@ "webpack/schema-utils/ajv-keywords": ["ajv-keywords@5.1.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3" }, "peerDependencies": { "ajv": "^8.8.2" } }, "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw=="], + "@remotion/renderer/source-map/whatwg-url/tr46": ["tr46@1.0.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA=="], + + "@remotion/renderer/source-map/whatwg-url/webidl-conversions": ["webidl-conversions@4.0.2", "", {}, "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg=="], + "@tailwindcss/vite/@tailwindcss/node/lightningcss/lightningcss-android-arm64": ["lightningcss-android-arm64@1.31.1", "", { "os": "android", "cpu": "arm64" }, "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg=="], "@tailwindcss/vite/@tailwindcss/node/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.31.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg=="], diff --git a/docs/public/api-reference/fabro-api.yaml b/docs/public/api-reference/fabro-api.yaml index 0c3f767c3..417acf422 100644 --- a/docs/public/api-reference/fabro-api.yaml +++ b/docs/public/api-reference/fabro-api.yaml @@ -7583,11 +7583,13 @@ components: type: ["string", "null"] format: date-time description: Wall-clock time the latest attempt of this stage started, if known. - duration_ms: - type: ["integer", "null"] - format: uint64 - minimum: 0 - description: Wall-clock duration of the stage's latest terminal attempt, if known. + timing: + oneOf: + - $ref: "#/components/schemas/StageTiming" + - type: "null" + description: | + Per-attempt timing breakdown for the latest terminal attempt: + wall time plus the active inference/tool breakdown. usage: $ref: "#/components/schemas/BilledTokenCounts" model: @@ -7743,17 +7745,15 @@ components: required: - stage_id - stage_label - - duration_ms + - timing - retries properties: stage_id: type: string stage_label: type: string - duration_ms: - type: integer - format: uint64 - minimum: 0 + timing: + $ref: "#/components/schemas/StageTiming" billing_usd_micros: type: ["integer", "null"] format: int64 @@ -7768,7 +7768,7 @@ components: required: - timestamp - status - - duration_ms + - timing - stages - total_retries - diff @@ -7778,10 +7778,8 @@ components: format: date-time status: $ref: "#/components/schemas/StageOutcome" - duration_ms: - type: integer - format: uint64 - minimum: 0 + timing: + $ref: "#/components/schemas/RunTiming" failure: oneOf: - $ref: "#/components/schemas/RunFailure" @@ -7908,6 +7906,7 @@ components: - models - source_directory - timestamps + - timing - billing - diff - pull_request @@ -7964,6 +7963,13 @@ components: type: ["string", "null"] timestamps: $ref: "#/components/schemas/RunTimestamps" + timing: + oneOf: + - $ref: "#/components/schemas/RunTiming" + - type: "null" + description: | + Run-level timing rollup. Wall time is the run's clock duration; + active timing sums work across stage visits. billing: oneOf: - $ref: "#/components/schemas/RunBillingSummary" @@ -8068,11 +8074,6 @@ components: completed_at: type: ["string", "null"] format: date-time - duration_ms: - type: ["integer", "null"] - format: int64 - elapsed_secs: - type: ["number", "null"] RunBillingSummary: type: object @@ -8212,10 +8213,12 @@ components: example: unit-tests status: $ref: "#/components/schemas/CheckRunStatus" - duration_secs: - type: number - description: Duration of the check run in seconds. - example: 154.0 + wall_time_ms: + type: integer + format: uint64 + minimum: 0 + description: Wall-clock duration of the check run in milliseconds. + example: 154000 # ── Reusable Sub-Schemas ─────────────────────────────────────────── @@ -8611,20 +8614,75 @@ components: format: uri example: https://github.com/fabro-sh/fabro/pull/123 - RunTimings: - description: Timing information for a run. + StageTiming: + description: | + Timing breakdown for one stage visit. Fields are all milliseconds. + `wall_time_ms` is elapsed clock time; `inference_time_ms` is Fabro- + observed LLM request/stream elapsed time; `tool_time_ms` is tool or + command execution elapsed time; `active_time_ms` equals + `inference_time_ms + tool_time_ms`. type: object required: - - elapsed_secs + - wall_time_ms + - active_time_ms properties: - elapsed_secs: - type: number - description: Wall-clock time elapsed in seconds. - example: 420.0 - elapsed_warning: - type: boolean - description: Whether the elapsed time exceeds the expected threshold. - example: false + wall_time_ms: + type: integer + format: uint64 + minimum: 0 + example: 1500 + inference_time_ms: + type: integer + format: uint64 + minimum: 0 + default: 0 + example: 900 + tool_time_ms: + type: integer + format: uint64 + minimum: 0 + default: 0 + example: 200 + active_time_ms: + type: integer + format: uint64 + minimum: 0 + description: Equals `inference_time_ms + tool_time_ms`. + example: 1100 + + RunTiming: + description: | + Timing rollup for an entire run. Active fields sum work across stage + visits, so `active_time_ms` can exceed `wall_time_ms` when parallel + branches run concurrently. + type: object + required: + - wall_time_ms + - active_time_ms + properties: + wall_time_ms: + type: integer + format: uint64 + minimum: 0 + example: 420000 + inference_time_ms: + type: integer + format: uint64 + minimum: 0 + default: 0 + example: 120000 + tool_time_ms: + type: integer + format: uint64 + minimum: 0 + default: 0 + example: 60000 + active_time_ms: + type: integer + format: uint64 + minimum: 0 + description: Equals `inference_time_ms + tool_time_ms`. + example: 180000 SandboxProvider: description: Sandbox execution provider. @@ -8704,7 +8762,7 @@ components: - reasoning_tokens - cache_read_tokens - cache_write_tokens - - runtime_secs + - timing properties: runs: type: integer @@ -8739,10 +8797,12 @@ components: format: int64 description: Total billed USD amount in micros. example: 20340000 - runtime_secs: - type: number - description: Total runtime in seconds. - example: 3501.0 + timing: + $ref: "#/components/schemas/RunTiming" + description: | + Aggregate timing rollup across every completed run. Active timing + sums work across stage visits, so `active_time_ms` can exceed + `wall_time_ms`. BillingStageRef: description: Reference to a workflow node in a billing stage row. @@ -8864,10 +8924,12 @@ components: $ref: "#/components/schemas/StageHandler" status: $ref: "#/components/schemas/StageState" - duration_secs: - type: number - description: Time spent in this stage, in seconds. - example: 154.0 + wall_time_ms: + type: integer + format: uint64 + minimum: 0 + description: Wall-clock time the latest attempt spent in this stage, in milliseconds. + example: 154000 node_id: type: string description: Node id in the workflow graph; multiple stages with different visits share the same node_id. @@ -9220,13 +9282,13 @@ components: # ── Billing Schemas ────────────────────────────────────────────────── RunBillingStage: - description: Token counts and billed totals for one workflow node within a run. Rows are grouped by node; billing and runtime sum every visit of that node. + description: Token counts and billed totals for one workflow node within a run. Rows are grouped by node; billing and timing sum every visit of that node. type: object required: - stage - model - billing - - runtime_secs + - timing properties: stage: $ref: "#/components/schemas/BillingStageRef" @@ -9237,10 +9299,11 @@ components: - type: "null" billing: $ref: "#/components/schemas/BilledTokenCounts" - runtime_secs: - type: number - description: Wall-clock runtime in seconds, summed across every visit of this node. - example: 154.0 + timing: + $ref: "#/components/schemas/StageTiming" + description: | + Per-node timing summed across every visit. `wall_time_ms` is the + sum of visit wall times; the active breakdown sums work timing. started_at: type: ["string", "null"] format: date-time @@ -9256,7 +9319,7 @@ components: description: Aggregate billing totals across all stages of a run. type: object required: - - runtime_secs + - timing - input_tokens - output_tokens - total_tokens @@ -9264,10 +9327,11 @@ components: - cache_read_tokens - cache_write_tokens properties: - runtime_secs: - type: number - description: Total wall-clock runtime in seconds. - example: 389.0 + timing: + $ref: "#/components/schemas/RunTiming" + description: | + Run-level timing rollup. `wall_time_ms` is summed across stage + visits; active timing sums work across visits. input_tokens: type: integer description: Total input tokens consumed. @@ -11350,4 +11414,4 @@ components: login: type: string description: User's login identifier (e.g. GitHub username). - example: octocat + example: octocat \ No newline at end of file diff --git a/lib/crates/fabro-api/build.rs b/lib/crates/fabro-api/build.rs index eb779ff40..9a5324342 100644 --- a/lib/crates/fabro-api/build.rs +++ b/lib/crates/fabro-api/build.rs @@ -364,6 +364,8 @@ fn main() { ("BillingModelRef", "fabro_model::ModelRef", &[]), ("BillingSpeed", "fabro_model::Speed", &[]), ("ExecOutputTail", "fabro_types::ExecOutputTail", &[]), + ("StageTiming", "fabro_types::StageTiming", &[]), + ("RunTiming", "fabro_types::RunTiming", &[]), ("ProviderId", "fabro_model::ProviderId", &[]), ("Model", "fabro_model::Model", &[]), ("Provider", "fabro_model::Provider", &[]), diff --git a/lib/crates/fabro-api/tests/run_billing_stage_round_trip.rs b/lib/crates/fabro-api/tests/run_billing_stage_round_trip.rs index 8acfd67af..be8d85747 100644 --- a/lib/crates/fabro-api/tests/run_billing_stage_round_trip.rs +++ b/lib/crates/fabro-api/tests/run_billing_stage_round_trip.rs @@ -37,7 +37,7 @@ fn run_billing_stage_model_accepts_required_null() { "cache_read_tokens": 0, "cache_write_tokens": 0 }, - "runtime_secs": 0.0 + "timing": {"wall_time_ms": 0, "inference_time_ms": 0, "tool_time_ms": 0, "active_time_ms": 0} }); let stage: RunBillingStage = @@ -69,7 +69,7 @@ fn run_billing_stage_round_trips_terminal_row_with_started_at_and_state() { "cache_read_tokens": 0, "cache_write_tokens": 0 }, - "runtime_secs": 5.5, + "timing": {"wall_time_ms": 5500, "inference_time_ms": 0, "tool_time_ms": 0, "active_time_ms": 0}, "started_at": "2026-04-29T12:34:56Z", "state": "succeeded" }); @@ -122,7 +122,7 @@ fn run_billing_stage_round_trips_in_flight_row() { "cache_read_tokens": 0, "cache_write_tokens": 0 }, - "runtime_secs": 1.25, + "timing": {"wall_time_ms": 1250, "inference_time_ms": 0, "tool_time_ms": 0, "active_time_ms": 0}, "started_at": "2026-04-29T12:34:56Z", "state": "running" }); diff --git a/lib/crates/fabro-api/tests/run_failure_round_trip.rs b/lib/crates/fabro-api/tests/run_failure_round_trip.rs index caeabcfb7..b3bd27baa 100644 --- a/lib/crates/fabro-api/tests/run_failure_round_trip.rs +++ b/lib/crates/fabro-api/tests/run_failure_round_trip.rs @@ -70,7 +70,7 @@ fn conclusion_json_uses_failure_object() { status: StageOutcome::Failed { retry_requested: false, }, - duration_ms: 42, + timing: fabro_types::RunTiming::wall_only(42), failure: Some(RunFailure { reason: FailureReason::WorkflowError, detail: FailureDetail::new("boom", FailureCategory::Deterministic), @@ -84,7 +84,12 @@ fn conclusion_json_uses_failure_object() { json!({ "timestamp": "2026-05-13T12:00:00Z", "status": "failed", - "duration_ms": 42, + "timing": { + "wall_time_ms": 42, + "inference_time_ms": 0, + "tool_time_ms": 0, + "active_time_ms": 0 + }, "failure": { "reason": "workflow_error", "detail": { diff --git a/lib/crates/fabro-api/tests/run_summary_round_trip.rs b/lib/crates/fabro-api/tests/run_summary_round_trip.rs index f5f283372..3b7887e72 100644 --- a/lib/crates/fabro-api/tests/run_summary_round_trip.rs +++ b/lib/crates/fabro-api/tests/run_summary_round_trip.rs @@ -6,7 +6,7 @@ use fabro_api::types::{RepositoryRef as ApiRepositoryRef, Run as ApiRun}; use fabro_types::status::{RunStatus, SuccessReason}; use fabro_types::{ DiffSummary, PullRequestLink, RepositoryProvider, RepositoryRef, Run, RunBillingSummary, RunId, - RunLifecycle, RunLinks, RunOrigin, RunTimestamps, WorkflowRef, + RunLifecycle, RunLinks, RunOrigin, RunTimestamps, RunTiming, WorkflowRef, }; use serde_json::json; @@ -62,9 +62,8 @@ fn run_summary_json_matches_openapi_shape() { started_at: Some(created_at), last_event_at: Some(last_event_at), completed_at: None, - duration_ms: Some(42_000), - elapsed_secs: Some(42.0), }, + timing: Some(RunTiming::new(42_000, 12_000, 30_000)), billing: Some(RunBillingSummary { total_usd_micros: Some(123), }), @@ -128,9 +127,13 @@ fn run_summary_json_matches_openapi_shape() { "created_at": "2026-04-20T12:00:00Z", "started_at": "2026-04-20T12:00:00Z", "last_event_at": "2026-04-20T12:00:42Z", - "completed_at": null, - "duration_ms": 42000, - "elapsed_secs": 42.0 + "completed_at": null + }, + "timing": { + "wall_time_ms": 42000, + "inference_time_ms": 12000, + "tool_time_ms": 30000, + "active_time_ms": 42000 }, "billing": { "total_usd_micros": 123 @@ -220,8 +223,7 @@ fn run_summary_deserializes_when_optional_fields_are_absent() { assert_eq!(summary.timestamps.last_event_at, None); assert_eq!(summary.lifecycle.status, RunStatus::Running); assert_eq!(summary.lifecycle.pending_control, None); - assert_eq!(summary.timestamps.duration_ms, None); - assert_eq!(summary.timestamps.elapsed_secs, None); + assert_eq!(summary.timing.map(|t| t.wall_time_ms), None); assert_eq!(summary.billing, None); assert_eq!(summary.superseded_by, None); assert_eq!(summary.diff, None); diff --git a/lib/crates/fabro-api/tests/stage_projection_round_trip.rs b/lib/crates/fabro-api/tests/stage_projection_round_trip.rs index 7d740316c..762bb9120 100644 --- a/lib/crates/fabro-api/tests/stage_projection_round_trip.rs +++ b/lib/crates/fabro-api/tests/stage_projection_round_trip.rs @@ -29,7 +29,12 @@ fn stage_projection_round_trips_representative_json() { "output": "ok", "termination": "exited", "started_at": "2026-04-29T12:34:00Z", - "duration_ms": 56000, + "timing": { + "wall_time_ms": 56000, + "inference_time_ms": 0, + "tool_time_ms": 0, + "active_time_ms": 0 + }, "usage": { "input_tokens": 0, "output_tokens": 0, diff --git a/lib/crates/fabro-cli/src/commands/run/events.rs b/lib/crates/fabro-cli/src/commands/run/events.rs index 061ecdb08..40cb62a52 100644 --- a/lib/crates/fabro-cli/src/commands/run/events.rs +++ b/lib/crates/fabro-cli/src/commands/run/events.rs @@ -372,7 +372,7 @@ fn format_event_pretty_value(envelope: &serde_json::Value, styles: &Styles) -> O } } "run.completed" => { - let duration = format_duration_ms(prop_field(envelope, "duration_ms")); + let duration = format_duration_ms(timing_wall_field(envelope)); let status_str = match prop_str_field(envelope, "status") { Some(status) if !status.is_empty() => status, _ => "succeeded", @@ -526,7 +526,7 @@ fn format_event_pretty_value(envelope: &serde_json::Value, styles: &Styles) -> O } "stage.completed" => { let label = str_field(envelope, "node_label").unwrap_or("?"); - let duration = format_duration_ms(prop_field(envelope, "duration_ms")); + let duration = format_duration_ms(timing_wall_field(envelope)); let billing = prop_field(envelope, "billing").or_else(|| prop_field(envelope, "usage")); let cost = format_cost( billing @@ -809,6 +809,13 @@ fn prop_str_field<'a>(value: &'a serde_json::Value, key: &str) -> Option<&'a str prop_field(value, key)?.as_str() } +/// Read `properties.timing.wall_time_ms` from a stage/run terminal event +/// envelope. Returns `None` when timing is absent, which falls back to a +/// blank display via `format_duration_ms`. +fn timing_wall_field(envelope: &serde_json::Value) -> Option<&serde_json::Value> { + prop_field(envelope, "timing")?.get("wall_time_ms") +} + fn failure_message(failure: &serde_json::Value) -> Option<&serde_json::Value> { failure .get("detail") @@ -1027,7 +1034,7 @@ mod tests { #[test] fn pretty_stage_completed() { let styles = no_color_styles(); - let line = r#"{"ts":"2026-01-01T14:23:15Z","event":"stage.completed","node_label":"plan","properties":{"duration_ms":8000,"status":"succeeded","usage":{"cost":0.12,"input_tokens":10000,"output_tokens":5200}}}"#; + let line = r#"{"ts":"2026-01-01T14:23:15Z","event":"stage.completed","node_label":"plan","properties":{"timing":{"wall_time_ms":8000,"inference_time_ms":0,"tool_time_ms":0,"active_time_ms":0},"status":"succeeded","usage":{"cost":0.12,"input_tokens":10000,"output_tokens":5200}}}"#; let result = format_event_pretty(line, &styles).unwrap(); assert!(result.contains("plan"), "got: {result}"); assert!(result.contains("$0.12"), "got: {result}"); @@ -1106,7 +1113,7 @@ mod tests { #[test] fn pretty_workflow_run_completed() { let styles = no_color_styles(); - let line = r#"{"ts":"2026-01-01T14:23:32Z","run_id":"abc123","event":"run.completed","properties":{"duration_ms":25000,"status":"succeeded","total_usd_micros":570000,"billing":{"input_tokens":5000,"output_tokens":2000,"total_tokens":7000,"cache_read_tokens":3000,"cache_write_tokens":500,"reasoning_tokens":800}}}"#; + let line = r#"{"ts":"2026-01-01T14:23:32Z","run_id":"abc123","event":"run.completed","properties":{"timing":{"wall_time_ms":25000,"inference_time_ms":0,"tool_time_ms":0,"active_time_ms":0},"status":"succeeded","total_usd_micros":570000,"billing":{"input_tokens":5000,"output_tokens":2000,"total_tokens":7000,"cache_read_tokens":3000,"cache_write_tokens":500,"reasoning_tokens":800}}}"#; let result = format_event_pretty(line, &styles).unwrap(); assert!(result.contains("SUCCEEDED"), "got: {result}"); assert!(result.contains("25s"), "got: {result}"); @@ -1120,7 +1127,7 @@ mod tests { #[test] fn pretty_workflow_run_completed_backward_compat() { let styles = no_color_styles(); - let line = r#"{"ts":"2026-01-01T14:23:32Z","run_id":"abc123","event":"run.completed","properties":{"duration_ms":25000,"total_cost":0.57}}"#; + let line = r#"{"ts":"2026-01-01T14:23:32Z","run_id":"abc123","event":"run.completed","properties":{"timing":{"wall_time_ms":25000,"inference_time_ms":0,"tool_time_ms":0,"active_time_ms":0},"total_cost":0.57}}"#; let result = format_event_pretty(line, &styles).unwrap(); assert!(result.contains("SUCCEEDED"), "got: {result}"); assert!(result.contains("25s"), "got: {result}"); @@ -1131,7 +1138,7 @@ mod tests { #[test] fn pretty_workflow_run_completed_fail_status() { let styles = no_color_styles(); - let line = r#"{"ts":"2026-01-01T14:23:32Z","event":"run.completed","properties":{"duration_ms":25000,"status":"failed"}}"#; + let line = r#"{"ts":"2026-01-01T14:23:32Z","event":"run.completed","properties":{"timing":{"wall_time_ms":25000,"inference_time_ms":0,"tool_time_ms":0,"active_time_ms":0},"status":"failed"}}"#; let result = format_event_pretty(line, &styles).unwrap(); assert!(result.contains("FAIL"), "got: {result}"); } diff --git a/lib/crates/fabro-cli/src/commands/run/output.rs b/lib/crates/fabro-cli/src/commands/run/output.rs index 33574c330..65e60d85c 100644 --- a/lib/crates/fabro-cli/src/commands/run/output.rs +++ b/lib/crates/fabro-cli/src/commands/run/output.rs @@ -205,7 +205,7 @@ pub(crate) fn print_run_conclusion( fabro_util::printerr!( printer, "Duration: {}", - HumanDuration(Duration::from_millis(conclusion.duration_ms)) + HumanDuration(Duration::from_millis(conclusion.timing.wall_time_ms)) ); if let Some(billing) = conclusion.billing.as_ref() { diff --git a/lib/crates/fabro-cli/src/commands/run/run_progress/event.rs b/lib/crates/fabro-cli/src/commands/run/run_progress/event.rs index fcb072d49..2b9d47123 100644 --- a/lib/crates/fabro-cli/src/commands/run/run_progress/event.rs +++ b/lib/crates/fabro-cli/src/commands/run/run_progress/event.rs @@ -132,11 +132,11 @@ pub(super) enum ProgressEvent { script: Option, }, StageCompleted { - node_id: String, - name: String, - duration_ms: u64, - status: String, - usage: Option, + node_id: String, + name: String, + timing: fabro_types::StageTiming, + status: String, + usage: Option, }, StageFailed { node_id: String, @@ -350,7 +350,7 @@ pub(super) fn from_run_event(stored: &RunEvent) -> Option { EventBody::StageCompleted(props) => Some(ProgressEvent::StageCompleted { node_id, name: node_label, - duration_ms: props.duration_ms, + timing: props.timing, status: props.status.to_string(), usage: props .billing @@ -550,7 +550,7 @@ mod tests { node_id: "plan".into(), name: "Plan".into(), index: 0, - duration_ms: 5000, + timing: fabro_types::StageTiming::wall_only(5000), status: "succeeded".into(), preferred_label: None, suggested_next_ids: Vec::new(), @@ -576,9 +576,9 @@ mod tests { ProgressEvent::StageCompleted { node_id, name, - duration_ms, + timing, .. - } if node_id == "plan" && name == "Plan" && duration_ms == 5000 + } if node_id == "plan" && name == "Plan" && timing.wall_time_ms == 5000 )); } diff --git a/lib/crates/fabro-cli/src/commands/run/run_progress/mod.rs b/lib/crates/fabro-cli/src/commands/run/run_progress/mod.rs index 4fea33577..ee2f9677f 100644 --- a/lib/crates/fabro-cli/src/commands/run/run_progress/mod.rs +++ b/lib/crates/fabro-cli/src/commands/run/run_progress/mod.rs @@ -255,7 +255,7 @@ impl ProgressUI { ProgressEvent::StageCompleted { node_id, name, - duration_ms, + timing, status, usage, } => { @@ -263,7 +263,7 @@ impl ProgressUI { renderer, &node_id, &name, - duration_ms, + timing.wall_time_ms, &status, usage.as_ref(), ); @@ -585,7 +585,7 @@ mod tests { node_id: node_id.into(), name: name.into(), index: 0, - duration_ms: 5000, + timing: fabro_types::StageTiming::wall_only(5000), status: "succeeded".into(), preferred_label: None, suggested_next_ids: Vec::new(), diff --git a/lib/crates/fabro-cli/src/commands/run/runner.rs b/lib/crates/fabro-cli/src/commands/run/runner.rs index 775731075..7cccfe674 100644 --- a/lib/crates/fabro-cli/src/commands/run/runner.rs +++ b/lib/crates/fabro-cli/src/commands/run/runner.rs @@ -850,7 +850,7 @@ mod tests { ); assert_eq!( worker_title_phase_for_event(&EventBody::RunCompleted(RunCompletedProps { - duration_ms: 10, + timing: fabro_types::RunTiming::wall_only(10), artifact_count: 0, status: "succeeded".to_string(), reason: SuccessReason::Completed, @@ -868,7 +868,7 @@ mod tests { reason: FailureReason::Cancelled, detail: FailureDetail::new("cancelled", FailureCategory::Canceled), }, - duration_ms: 10, + timing: fabro_types::RunTiming::wall_only(10), final_git_commit_sha: None, final_patch: None, diff_summary: None, @@ -882,7 +882,7 @@ mod tests { reason: FailureReason::Terminated, detail: FailureDetail::new("boom", FailureCategory::Deterministic), }, - duration_ms: 10, + timing: fabro_types::RunTiming::wall_only(10), final_git_commit_sha: None, final_patch: None, diff_summary: None, diff --git a/lib/crates/fabro-cli/src/commands/run/wait.rs b/lib/crates/fabro-cli/src/commands/run/wait.rs index 94c94b34e..6ef61c4ee 100644 --- a/lib/crates/fabro-cli/src/commands/run/wait.rs +++ b/lib/crates/fabro-cli/src/commands/run/wait.rs @@ -82,7 +82,8 @@ fn build_json_output( "status": run_status_kind(status), }); if let Some(c) = conclusion { - value["duration_ms"] = c.duration_ms.into(); + value["timing"] = + serde_json::to_value(c.timing).unwrap_or_else(|_| serde_json::Value::Null); if let Some(total_usd_micros) = c .billing .as_ref() @@ -112,7 +113,7 @@ fn print_human_output( let details = match conclusion { Some(c) => { - let duration = format_duration_ms(c.duration_ms); + let duration = format_duration_ms(c.timing.wall_time_ms); let cost = c .billing .as_ref() @@ -152,7 +153,7 @@ mod tests { let conclusion = Conclusion { timestamp: chrono::Utc::now(), status: StageOutcome::Succeeded, - duration_ms: 12345, + timing: fabro_types::RunTiming::wall_only(12345), failure: None, final_git_commit_sha: None, stages: vec![], @@ -177,7 +178,7 @@ mod tests { ); assert_eq!(json["run_id"], run_id.to_string()); assert_eq!(json["status"], "succeeded"); - assert_eq!(json["duration_ms"], 12345); + assert_eq!(json["timing"]["wall_time_ms"], 12345); assert_eq!(json["total_usd_micros"], 420_000); } @@ -193,7 +194,7 @@ mod tests { ); assert_eq!(json["run_id"], run_id.to_string()); assert_eq!(json["status"], "failed"); - assert!(json.get("duration_ms").is_none()); + assert!(json.get("timing").is_none()); assert!(json.get("total_usd_micros").is_none()); } @@ -211,7 +212,7 @@ mod tests { status: StageOutcome::Failed { retry_requested: false, }, - duration_ms: 500, + timing: fabro_types::RunTiming::wall_only(500), failure: Some(RunFailure { reason: FailureReason::WorkflowError, detail: FailureDetail::new("error", FailureCategory::Deterministic), @@ -230,7 +231,7 @@ mod tests { Some(&conclusion), ); assert!(json.get("total_usd_micros").is_none()); - assert_eq!(json["duration_ms"], 500); + assert_eq!(json["timing"]["wall_time_ms"], 500); } #[test] @@ -240,7 +241,7 @@ mod tests { let conclusion = Conclusion { timestamp: chrono::Utc::now(), status: StageOutcome::Succeeded, - duration_ms: 8000, + timing: fabro_types::RunTiming::wall_only(8000), failure: None, final_git_commit_sha: None, stages: vec![], diff --git a/lib/crates/fabro-cli/src/commands/runs/list.rs b/lib/crates/fabro-cli/src/commands/runs/list.rs index 93ccab216..e545efd67 100644 --- a/lib/crates/fabro-cli/src/commands/runs/list.rs +++ b/lib/crates/fabro-cli/src/commands/runs/list.rs @@ -54,7 +54,7 @@ pub(crate) async fn list_command( "status": run.status(), "start_time": run.start_time(), "labels": run.labels(), - "duration_ms": run.duration_ms(), + "wall_time_ms": run.wall_time_ms(), "total_usd_micros": run.total_usd_micros(), "source_directory": run.source_directory(), "repo_origin_url": run.repo_origin_url(), @@ -107,7 +107,7 @@ pub(crate) async fn list_command( let rows: Vec> = display_runs .iter() .map(|run| { - let duration_display = match run.duration_ms() { + let duration_display = match run.wall_time_ms() { Some(ms) => format_duration_ms(ms), None => match run.start_time_dt() { Some(start) => { diff --git a/lib/crates/fabro-cli/src/server_runs.rs b/lib/crates/fabro-cli/src/server_runs.rs index 2a4b20543..adb2f9566 100644 --- a/lib/crates/fabro-cli/src/server_runs.rs +++ b/lib/crates/fabro-cli/src/server_runs.rs @@ -77,8 +77,8 @@ impl ServerRunInfo { &self.run.labels } - pub(crate) fn duration_ms(&self) -> Option { - self.run.timestamps.duration_ms + pub(crate) fn wall_time_ms(&self) -> Option { + self.run.timing.as_ref().map(|t| t.wall_time_ms) } pub(crate) fn total_usd_micros(&self) -> Option { diff --git a/lib/crates/fabro-cli/tests/it/cmd/attach.rs b/lib/crates/fabro-cli/tests/it/cmd/attach.rs index 0995ae15d..285793b9d 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/attach.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/attach.rs @@ -1147,13 +1147,18 @@ fn attach_json_errors_without_prompting_for_human_input() { "internal.run_id": "[ULID]", "internal.thread_id": null }, - "duration_ms": "[DURATION_MS]", "index": 0, "max_attempts": 1, "node_visits": { "start": 1 }, - "status": "succeeded" + "status": "succeeded", + "timing": { + "active_time_ms": 0, + "inference_time_ms": 0, + "tool_time_ms": 0, + "wall_time_ms": 0 + } }, "run_id": "[ULID]", "stage_id": "start@1", diff --git a/lib/crates/fabro-cli/tests/it/cmd/inspect.rs b/lib/crates/fabro-cli/tests/it/cmd/inspect.rs index 8d212c4b1..f060a2979 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/inspect.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/inspect.rs @@ -356,7 +356,7 @@ fn inspect_completed_run_shows_run_start_conclusion_checkpoint() { }, "conclusion": { "status": "succeeded", - "duration_ms": "[DURATION_MS]", + "timing": "[TIMING]", "stage_count": null }, "checkpoint": { @@ -428,7 +428,7 @@ fn inspect_completed_run_reads_store_without_disk_metadata_files() { }, "conclusion": { "status": "succeeded", - "duration_ms": "[DURATION_MS]", + "timing": "[TIMING]", "stage_count": null }, "checkpoint": { @@ -487,7 +487,7 @@ fn inspect_git_backed_run_exposes_checkpoint_and_sandbox_state() { }, "conclusion": { "status": "succeeded", - "duration_ms": "[DURATION_MS]", + "timing": "[TIMING]", "final_git_commit_sha": "[SHA]", "stage_count": null }, diff --git a/lib/crates/fabro-cli/tests/it/cmd/run.rs b/lib/crates/fabro-cli/tests/it/cmd/run.rs index c86788e4b..464c7cb95 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/run.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/run.rs @@ -57,7 +57,7 @@ fn remote_run_state_response(run_id: &str) -> serde_json::Value { state["conclusion"] = serde_json::json!({ "timestamp": "2026-04-05T12:00:01Z", "status": "succeeded", - "duration_ms": 12, + "timing": {"wall_time_ms": 12, "inference_time_ms": 0, "tool_time_ms": 0, "active_time_ms": 0}, "stages": [], "billing": null, "total_retries": 0, @@ -74,7 +74,7 @@ fn run_completed_event(run_id: &str) -> serde_json::Value { "run_id": run_id, "ts": "2026-04-05T12:00:01Z", "properties": { - "duration_ms": 12, + "timing": {"wall_time_ms": 12, "inference_time_ms": 0, "tool_time_ms": 0, "active_time_ms": 0}, "artifact_count": 0, "status": "succeeded", "reason": "completed" diff --git a/lib/crates/fabro-cli/tests/it/cmd/support.rs b/lib/crates/fabro-cli/tests/it/cmd/support.rs index 660d5534b..e2ab6ef22 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/support.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/support.rs @@ -174,10 +174,9 @@ pub(crate) fn remote_run_summary_json( "created_at": timestamp, "started_at": timestamp, "last_event_at": null, - "completed_at": null, - "duration_ms": null, - "elapsed_secs": null + "completed_at": null }, + "timing": null, "billing": null, "diff": null, "pull_request": null, @@ -1108,7 +1107,7 @@ async fn append_seeded_simple_completion_events( None, "run.completed", serde_json::json!({ - "duration_ms": 123, + "timing": {"wall_time_ms": 123, "inference_time_ms": 0, "tool_time_ms": 0, "active_time_ms": 0}, "artifact_count": 0, "status": "succeeded", "reason": "completed", @@ -1274,7 +1273,7 @@ async fn append_seeded_git_completion_events( None, "run.completed", serde_json::json!({ - "duration_ms": 456, + "timing": {"wall_time_ms": 456, "inference_time_ms": 0, "tool_time_ms": 0, "active_time_ms": 0}, "artifact_count": 0, "status": "succeeded", "reason": "completed", @@ -1335,7 +1334,7 @@ async fn append_seeded_git_noop_events( None, "run.completed", serde_json::json!({ - "duration_ms": 123, + "timing": {"wall_time_ms": 123, "inference_time_ms": 0, "tool_time_ms": 0, "active_time_ms": 0}, "artifact_count": 0, "status": "succeeded", "reason": "completed", @@ -1395,7 +1394,7 @@ async fn append_seeded_artifact_run_events( None, "run.completed", serde_json::json!({ - "duration_ms": 123, + "timing": {"wall_time_ms": 123, "inference_time_ms": 0, "tool_time_ms": 0, "active_time_ms": 0}, "artifact_count": 6, "status": "succeeded", "reason": "completed", @@ -1544,7 +1543,7 @@ fn test_labels(context: &TestContext) -> Vec { fn stage_completed_properties(index: usize, response: Option<&str>) -> serde_json::Value { serde_json::json!({ "index": index, - "duration_ms": 1, + "timing": {"wall_time_ms": 1, "inference_time_ms": 0, "tool_time_ms": 0, "active_time_ms": 0}, "status": "succeeded", "preferred_label": null, "suggested_next_ids": [], @@ -1741,7 +1740,7 @@ pub(crate) fn compact_inspect(output: &Output) -> Value { "conclusion": conclusion.as_object().map(|_| { serde_json::json!({ "status": conclusion["status"], - "duration_ms": "[DURATION_MS]", + "timing": "[TIMING]", "stage_count": conclusion["stages"].as_array().map(|stages| stages.len()), }) }), @@ -1802,7 +1801,7 @@ pub(crate) fn compact_git_inspect(output: &Output) -> Value { "conclusion": conclusion.as_object().map(|_| { serde_json::json!({ "status": conclusion["status"], - "duration_ms": "[DURATION_MS]", + "timing": "[TIMING]", "final_git_commit_sha": "[SHA]", "stage_count": conclusion["stages"].as_array().map(|stages| stages.len()), }) diff --git a/lib/crates/fabro-cli/tests/it/cmd/wait.rs b/lib/crates/fabro-cli/tests/it/cmd/wait.rs index dffa0b799..792b78284 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/wait.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/wait.rs @@ -96,8 +96,20 @@ fn wait_completed_run_json_outputs_status_and_duration() { let run = setup_seeded_completed_dry_run(&context); let mut filters = context.filters(); filters.push(( - r#""duration_ms":\s*\d+"#.to_string(), - r#""duration_ms": [DURATION_MS]"#.to_string(), + r#""wall_time_ms":\s*\d+"#.to_string(), + r#""wall_time_ms": [WALL_TIME_MS]"#.to_string(), + )); + filters.push(( + r#""inference_time_ms":\s*\d+"#.to_string(), + r#""inference_time_ms": [INFERENCE_TIME_MS]"#.to_string(), + )); + filters.push(( + r#""tool_time_ms":\s*\d+"#.to_string(), + r#""tool_time_ms": [TOOL_TIME_MS]"#.to_string(), + )); + filters.push(( + r#""active_time_ms":\s*\d+"#.to_string(), + r#""active_time_ms": [ACTIVE_TIME_MS]"#.to_string(), )); let mut cmd = context.command(); cmd.args(["wait", "--json", &run.run_id]); @@ -109,7 +121,12 @@ fn wait_completed_run_json_outputs_status_and_duration() { { "run_id": "[ULID]", "status": "succeeded", - "duration_ms": [DURATION_MS] + "timing": { + "wall_time_ms": [WALL_TIME_MS], + "inference_time_ms": [INFERENCE_TIME_MS], + "tool_time_ms": [TOOL_TIME_MS], + "active_time_ms": [ACTIVE_TIME_MS] + } } ----- stderr ----- "###); diff --git a/lib/crates/fabro-cli/tests/it/scenario/smoke.rs b/lib/crates/fabro-cli/tests/it/scenario/smoke.rs index 0edc31c18..08e56acb8 100644 --- a/lib/crates/fabro-cli/tests/it/scenario/smoke.rs +++ b/lib/crates/fabro-cli/tests/it/scenario/smoke.rs @@ -21,7 +21,7 @@ fn run_sse_body(run_id: &str) -> String { "run_id": run_id, "ts": "2026-04-05T12:00:01Z", "properties": { - "duration_ms": 12, + "timing": {"wall_time_ms": 12, "inference_time_ms": 0, "tool_time_ms": 0, "active_time_ms": 0}, "artifact_count": 0, "status": "succeeded", "reason": "completed" diff --git a/lib/crates/fabro-cli/tests/it/workflow/full_stack.rs b/lib/crates/fabro-cli/tests/it/workflow/full_stack.rs index 3ba6c9caa..e70dea83a 100644 --- a/lib/crates/fabro-cli/tests/it/workflow/full_stack.rs +++ b/lib/crates/fabro-cli/tests/it/workflow/full_stack.rs @@ -37,8 +37,8 @@ fn scenario_full_stack(sandbox: &str) { "conclusion: {conclusion}" ); assert!( - conclusion["duration_ms"].as_u64().unwrap_or(0) > 0, - "duration_ms should be > 0" + conclusion["timing"]["wall_time_ms"].as_u64().unwrap_or(0) > 0, + "timing.wall_time_ms should be > 0" ); // RunSpec should have key fields diff --git a/lib/crates/fabro-core/src/executor.rs b/lib/crates/fabro-core/src/executor.rs index 99fa5bb52..19cf142a1 100644 --- a/lib/crates/fabro-core/src/executor.rs +++ b/lib/crates/fabro-core/src/executor.rs @@ -14,9 +14,37 @@ use crate::lifecycle::{ AttemptContext, AttemptResultContext, EdgeContext, EdgeDecision, NodeDecision, NoopLifecycle, RunLifecycle, }; -use crate::outcome::{NodeResult, NodeResultExt, Outcome}; +use crate::outcome::{NodeResult, NodeResultExt, Outcome, OutcomeMeta}; use crate::state::ExecutionState; +/// Build a [`NodeResult`] from an attempt outcome, pulling the inference and +/// tool breakdown from `outcome.timing` when handlers populated it. The wall +/// time comes from the executor's stopwatch since that is the source of +/// authoritative per-attempt clock time. +fn node_result_from_outcome( + outcome: Outcome, + wall_time: std::time::Duration, + attempts: u32, + max_attempts: u32, +) -> NodeResult { + let inference_time = outcome + .timing + .map(|t| std::time::Duration::from_millis(t.inference_time_ms)) + .unwrap_or_default(); + let tool_time = outcome + .timing + .map(|t| std::time::Duration::from_millis(t.tool_time_ms)) + .unwrap_or_default(); + NodeResult::new( + outcome, + wall_time, + inference_time, + tool_time, + attempts, + max_attempts, + ) +} + #[derive(Default)] pub struct ExecutorOptions { pub cancel_token: Option, @@ -269,9 +297,9 @@ impl Executor { graph: &G, ) -> Result> { let policy = self.handler.retry_policy(node, graph); - let start = Instant::now(); for attempt in 1..=policy.max_attempts { + let attempt_start = Instant::now(); let attempt_ctx = AttemptContext { node, attempt, @@ -288,8 +316,12 @@ impl Executor { match self.handler.execute(node, &state.context, graph).await { Ok(outcome) if outcome.status.retry_requested() && can_retry => { let delay = policy.backoff.delay_for_attempt(attempt); - let result = - NodeResult::new(outcome, start.elapsed(), attempt, policy.max_attempts); + let result = node_result_from_outcome( + outcome, + attempt_start.elapsed(), + attempt, + policy.max_attempts, + ); let ctx = AttemptResultContext { node, result: &result, @@ -302,9 +334,9 @@ impl Executor { } Ok(outcome) if outcome.status.retry_requested() => { let final_outcome = self.handler.on_retries_exhausted(node, outcome); - let result = NodeResult::new( + let result = node_result_from_outcome( final_outcome, - start.elapsed(), + attempt_start.elapsed(), attempt, policy.max_attempts, ); @@ -319,8 +351,12 @@ impl Executor { return Ok(result); } Ok(outcome) => { - let result = - NodeResult::new(outcome, start.elapsed(), attempt, policy.max_attempts); + let result = node_result_from_outcome( + outcome, + attempt_start.elapsed(), + attempt, + policy.max_attempts, + ); let ctx = AttemptResultContext { node, result: &result, @@ -333,8 +369,12 @@ impl Executor { } Err(e) if can_retry && e.is_retryable() => { let delay = policy.backoff.delay_for_attempt(attempt); - let fail_result = - NodeResult::from_error(&e, start.elapsed(), attempt, policy.max_attempts); + let fail_result = NodeResult::from_error( + &e, + attempt_start.elapsed(), + attempt, + policy.max_attempts, + ); let ctx = AttemptResultContext { node, result: &fail_result, @@ -348,8 +388,12 @@ impl Executor { Err(e @ Error::Handler { .. }) => { // Convert handler failures to fail outcomes so routing continues. let outcome = e.to_fail_outcome(); - let result = - NodeResult::new(outcome, start.elapsed(), attempt, policy.max_attempts); + let result = node_result_from_outcome( + outcome, + attempt_start.elapsed(), + attempt, + policy.max_attempts, + ); let ctx = AttemptResultContext { node, result: &result, @@ -1289,6 +1333,80 @@ mod tests { assert_eq!(log, vec![(1, true), (2, false)]); } + #[tokio::test] + async fn executor_retry_attempt_wall_time_excludes_prior_attempts_and_backoff() { + let wall_times = Arc::new(Mutex::new(Vec::::new())); + + struct WallTimeTracker(Arc>>); + #[async_trait] + impl RunLifecycle for WallTimeTracker { + async fn after_attempt( + &self, + ctx: &AttemptResultContext<'_, TestGraph>, + _s: &ExecutionState, + ) -> Result<()> { + self.0.lock().unwrap().push(ctx.result.wall_time); + Ok(()) + } + } + + struct SlowRetryThenSuccess(AtomicU32); + #[async_trait] + impl NodeHandler for SlowRetryThenSuccess { + async fn execute( + &self, + _n: &TestNode, + _c: &Context, + _g: &TestGraph, + ) -> Result { + sleep(Duration::from_millis(5)).await; + let call = self.0.fetch_add(1, Ordering::Relaxed); + if call == 0 { + Ok(Outcome { + status: StageOutcome::Failed { + retry_requested: true, + }, + ..Outcome::default() + }) + } else { + Ok(Outcome::success()) + } + } + + fn retry_policy(&self, _n: &TestNode, _g: &TestGraph) -> RetryPolicy { + RetryPolicy { + max_attempts: 2, + backoff: BackoffPolicy { + initial_delay: Duration::from_millis(500), + factor: 1.0, + max_delay: Duration::from_millis(500), + jitter: false, + }, + } + } + } + + let g = linear_graph(&["start", "end"]); + let state = ExecutionState::new(&g).unwrap(); + let executor = + ExecutorBuilder::new(Arc::new(SlowRetryThenSuccess(AtomicU32::new(0))) + as Arc>) + .lifecycle(Box::new(WallTimeTracker(Arc::clone(&wall_times)))) + .build(); + + executor.run(&g, state).await.unwrap(); + + let wall_times = wall_times.lock().unwrap().clone(); + assert_eq!(wall_times.len(), 2); + for wall_time in wall_times { + assert!(wall_time >= Duration::from_millis(5)); + assert!( + wall_time < Duration::from_millis(300), + "attempt wall time should not include retry backoff or prior attempts: {wall_time:?}" + ); + } + } + #[tokio::test] async fn executor_retry_lifecycle_before_attempt_skip_stops_retry() { let call_count = Arc::new(std::sync::atomic::AtomicU32::new(0)); diff --git a/lib/crates/fabro-core/src/lifecycle.rs b/lib/crates/fabro-core/src/lifecycle.rs index f122c426c..6fc61c087 100644 --- a/lib/crates/fabro-core/src/lifecycle.rs +++ b/lib/crates/fabro-core/src/lifecycle.rs @@ -570,7 +570,14 @@ mod tests { let g = linear_graph(&["start", "end"]); let state = ExecutionState::new(&g).unwrap(); let node = g.get_node("start").unwrap(); - let result = NodeResult::new(Outcome::success(), Duration::ZERO, 1, 1); + let result = NodeResult::new( + Outcome::success(), + Duration::ZERO, + Duration::ZERO, + Duration::ZERO, + 1, + 1, + ); let ctx = AttemptResultContext { node: &node, result: &result, @@ -667,7 +674,14 @@ mod tests { let g = linear_graph(&["start", "end"]); let state = ExecutionState::new(&g).unwrap(); let node = g.get_node("start").unwrap(); - let mut result = NodeResult::new(Outcome::success(), Duration::ZERO, 1, 1); + let mut result = NodeResult::new( + Outcome::success(), + Duration::ZERO, + Duration::ZERO, + Duration::ZERO, + 1, + 1, + ); lc.after_node(&node, &mut result, &state).await.unwrap(); let calls = log.lock().unwrap().clone(); assert_eq!(calls, vec!["a:after_node", "b:after_node"]); @@ -683,7 +697,14 @@ mod tests { let g = linear_graph(&["start", "end"]); let state = ExecutionState::new(&g).unwrap(); let node = g.get_node("start").unwrap(); - let result = NodeResult::new(Outcome::success(), Duration::ZERO, 1, 1); + let result = NodeResult::new( + Outcome::success(), + Duration::ZERO, + Duration::ZERO, + Duration::ZERO, + 1, + 1, + ); lc.after_record(&node, &result, &state).await.unwrap(); let calls = log.lock().unwrap().clone(); assert_eq!(calls, vec!["a:after_record", "b:after_record"]); diff --git a/lib/crates/fabro-core/src/outcome.rs b/lib/crates/fabro-core/src/outcome.rs index 53d590fbf..794efae79 100644 --- a/lib/crates/fabro-core/src/outcome.rs +++ b/lib/crates/fabro-core/src/outcome.rs @@ -7,14 +7,16 @@ pub use fabro_types::outcome::{ use crate::error::Error; pub trait NodeResultExt { - fn from_error(error: &Error, duration: Duration, attempts: u32, max_attempts: u32) -> Self; + fn from_error(error: &Error, wall_time: Duration, attempts: u32, max_attempts: u32) -> Self; } impl NodeResultExt for NodeResult { - fn from_error(error: &Error, duration: Duration, attempts: u32, max_attempts: u32) -> Self { + fn from_error(error: &Error, wall_time: Duration, attempts: u32, max_attempts: u32) -> Self { Self { outcome: error.to_fail_outcome(), - duration, + wall_time, + inference_time: Duration::ZERO, + tool_time: Duration::ZERO, attempts, max_attempts, } diff --git a/lib/crates/fabro-core/src/state.rs b/lib/crates/fabro-core/src/state.rs index af37e89dd..ca3b8e56b 100644 --- a/lib/crates/fabro-core/src/state.rs +++ b/lib/crates/fabro-core/src/state.rs @@ -111,7 +111,14 @@ mod tests { fn run_state_record_updates_all_fields() { let g = linear_graph(&["start", "end"]); let mut state = ExecutionState::<()>::new(&g).unwrap(); - let result = NodeResult::new(Outcome::success(), Duration::from_millis(50), 2, 3); + let result = NodeResult::new( + Outcome::success(), + Duration::from_millis(50), + Duration::ZERO, + Duration::ZERO, + 2, + 3, + ); state.record("start", &result); assert_eq!(state.completed_nodes, vec!["start"]); @@ -126,7 +133,14 @@ mod tests { let mut state = ExecutionState::<()>::new(&g).unwrap(); let mut outcome = Outcome::success(); outcome.context_updates.insert("key".into(), json!("value")); - let result = NodeResult::new(outcome, Duration::ZERO, 1, 1); + let result = NodeResult::new( + outcome, + Duration::ZERO, + Duration::ZERO, + Duration::ZERO, + 1, + 1, + ); state.record("start", &result); assert_eq!(state.context.get("key"), Some(json!("value"))); } @@ -155,7 +169,14 @@ mod tests { state.increment_visits("work"); state.record( "start", - &NodeResult::new(Outcome::success(), Duration::ZERO, 1, 1), + &NodeResult::new( + Outcome::success(), + Duration::ZERO, + Duration::ZERO, + Duration::ZERO, + 1, + 1, + ), ); state.advance("work"); diff --git a/lib/crates/fabro-dump/src/lib.rs b/lib/crates/fabro-dump/src/lib.rs index 2a023f2f2..8345b84a4 100644 --- a/lib/crates/fabro-dump/src/lib.rs +++ b/lib/crates/fabro-dump/src/lib.rs @@ -548,7 +548,7 @@ mod tests { .single() .unwrap(), status: StageOutcome::Succeeded, - duration_ms: 5, + timing: fabro_types::RunTiming::wall_only(5), failure: None, final_git_commit_sha: Some("abc123".to_string()), stages: Vec::new(), diff --git a/lib/crates/fabro-server/src/demo/mod.rs b/lib/crates/fabro-server/src/demo/mod.rs index f42fffff2..d3f2463db 100644 --- a/lib/crates/fabro-server/src/demo/mod.rs +++ b/lib/crates/fabro-server/src/demo/mod.rs @@ -1106,7 +1106,8 @@ mod runs { let run_id = RunId::with_timestamp(created_at, sequence); let source_directory = Some(format!("/demo/{repo_name}")); let repo_origin_url = Some(format!("https://github.com/demo/{repo_name}.git")); - let duration_ms = elapsed_secs.and_then(duration_ms_from_secs); + let wall_time_ms = elapsed_secs.and_then(duration_ms_from_secs); + let timing = wall_time_ms.map(fabro_types::RunTiming::wall_only); Run { id: run_id, parent_id: None, @@ -1145,9 +1146,8 @@ mod runs { started_at: Some(created_at), last_event_at: Some(created_at), completed_at: Some(created_at), - duration_ms, - elapsed_secs, }, + timing, billing: total_usd_micros.map(|total_usd_micros| RunBillingSummary { total_usd_micros: Some(total_usd_micros), }), @@ -1350,7 +1350,7 @@ mod runs { &StageId::new("detect-drift", 1), "Detect Drift", StageState::Succeeded, - Some(72.0), + Some(72_000), None, StageHandler::Command, ), @@ -1358,7 +1358,7 @@ mod runs { &StageId::new("propose-changes", 1), "Propose Changes", StageState::Succeeded, - Some(154.0), + Some(154_000), None, StageHandler::Agent, ), @@ -1366,7 +1366,7 @@ mod runs { &StageId::new("review-changes", 1), "Review Changes", StageState::Succeeded, - Some(45.0), + Some(45_000), None, StageHandler::Agent, ), @@ -1374,7 +1374,7 @@ mod runs { &StageId::new("apply-changes", 1), "Apply Changes", StageState::Succeeded, - Some(118.0), + Some(118_000), None, StageHandler::Command, ), @@ -1512,15 +1512,15 @@ mod runs { RunBilling { stages: vec![ RunBillingStage { - stage: BillingStageRef { + stage: BillingStageRef { id: "detect-drift".into(), name: "Detect Drift".into(), }, - model: Some(billing_model( + model: Some(billing_model( fabro_model::ProviderId::anthropic(), "claude-opus-4-6", )), - billing: BilledTokenCounts { + billing: BilledTokenCounts { cache_read_tokens: 0, cache_write_tokens: 0, input_tokens: 12480, @@ -1529,20 +1529,20 @@ mod runs { total_tokens: 15690, total_usd_micros: Some(480_000), }, - runtime_secs: 72.0, - started_at: None, - state: Some(StageState::Succeeded), + timing: fabro_types::StageTiming::wall_only(72_000), + started_at: None, + state: Some(StageState::Succeeded), }, RunBillingStage { - stage: BillingStageRef { + stage: BillingStageRef { id: "propose-changes".into(), name: "Propose Changes".into(), }, - model: Some(billing_model( + model: Some(billing_model( fabro_model::ProviderId::gemini(), "gemini-3.1-pro-preview", )), - billing: BilledTokenCounts { + billing: BilledTokenCounts { cache_read_tokens: 0, cache_write_tokens: 0, input_tokens: 28640, @@ -1551,20 +1551,20 @@ mod runs { total_tokens: 37390, total_usd_micros: Some(720_000), }, - runtime_secs: 154.0, - started_at: None, - state: Some(StageState::Succeeded), + timing: fabro_types::StageTiming::wall_only(154_000), + started_at: None, + state: Some(StageState::Succeeded), }, RunBillingStage { - stage: BillingStageRef { + stage: BillingStageRef { id: "review-changes".into(), name: "Review Changes".into(), }, - model: Some(billing_model( + model: Some(billing_model( fabro_model::ProviderId::openai(), "gpt-5.3-codex", )), - billing: BilledTokenCounts { + billing: BilledTokenCounts { cache_read_tokens: 0, cache_write_tokens: 0, input_tokens: 9120, @@ -1573,20 +1573,20 @@ mod runs { total_tokens: 11760, total_usd_micros: Some(190_000), }, - runtime_secs: 45.0, - started_at: None, - state: Some(StageState::Succeeded), + timing: fabro_types::StageTiming::wall_only(45_000), + started_at: None, + state: Some(StageState::Succeeded), }, RunBillingStage { - stage: BillingStageRef { + stage: BillingStageRef { id: "apply-changes".into(), name: "Apply Changes".into(), }, - model: Some(billing_model( + model: Some(billing_model( fabro_model::ProviderId::anthropic(), "claude-opus-4-6", )), - billing: BilledTokenCounts { + billing: BilledTokenCounts { cache_read_tokens: 0, cache_write_tokens: 0, input_tokens: 21300, @@ -1595,15 +1595,15 @@ mod runs { total_tokens: 27780, total_usd_micros: Some(870_000), }, - runtime_secs: 118.0, - started_at: None, - state: Some(StageState::Running), + timing: fabro_types::StageTiming::wall_only(118_000), + started_at: None, + state: Some(StageState::Running), }, ], totals: RunBillingTotals { cache_read_tokens: 0, cache_write_tokens: 0, - runtime_secs: 389.0, + timing: fabro_types::RunTiming::wall_only(389_000), input_tokens: 71540, output_tokens: 21080, reasoning_tokens: 0, @@ -1987,7 +1987,7 @@ mod billing { input_tokens: 643_860, output_tokens: 189_720, reasoning_tokens: 0, - runtime_secs: 3_501.0, + timing: fabro_types::RunTiming::wall_only(3_501_000), total_tokens: 833_580, total_usd_micros: Some(20_340_000), }, diff --git a/lib/crates/fabro-server/src/run_files.rs b/lib/crates/fabro-server/src/run_files.rs index d93c02d69..835b386c4 100644 --- a/lib/crates/fabro-server/src/run_files.rs +++ b/lib/crates/fabro-server/src/run_files.rs @@ -2390,7 +2390,7 @@ index 1111111..2222222 160000 projection.conclusion = Some(fabro_types::Conclusion { timestamp: chrono::Utc::now(), status: fabro_types::StageOutcome::Succeeded, - duration_ms: 1, + timing: fabro_types::RunTiming::wall_only(1), failure: None, final_git_commit_sha: None, stages: Vec::new(), diff --git a/lib/crates/fabro-server/src/server.rs b/lib/crates/fabro-server/src/server.rs index 7f2fb6a3a..d016c9755 100644 --- a/lib/crates/fabro-server/src/server.rs +++ b/lib/crates/fabro-server/src/server.rs @@ -252,9 +252,9 @@ struct ModelBillingTotals { /// In-memory aggregate billing counters, reset on server restart. #[derive(Default)] struct BillingAccumulator { - total_runs: i64, - total_runtime_secs: f64, - by_model: HashMap, + total_runs: i64, + total_timing: fabro_types::RunTiming, + by_model: HashMap, } pub(crate) type RegistryFactoryOverride = @@ -703,7 +703,7 @@ fn accumulate_billing_rollup( rollup: &fabro_workflow::ProjectionBillingRollup, ) { accumulator.total_runs += 1; - accumulator.total_runtime_secs += rollup.runtime_ms as f64 / 1000.0; + accumulator.total_timing = accumulator.total_timing.saturating_add(&rollup.timing); for model in &rollup.by_model { let entry = accumulator.by_model.entry(model.model.clone()).or_default(); entry.stages += model.stages; @@ -715,7 +715,7 @@ pub(crate) fn run_stage_from_stage_id( stage_id: &StageId, name: impl Into, status: StageState, - duration_secs: Option, + wall_time_ms: Option, started_at: Option>, handler: StageHandler, ) -> RunStage { @@ -724,7 +724,7 @@ pub(crate) fn run_stage_from_stage_id( name: name.into(), handler, status, - duration_secs, + wall_time_ms, node_id: stage_id.node_id().to_string(), visit: std::num::NonZeroU32::new(stage_id.visit()) .expect("StageId stores a non-zero visit"), @@ -2237,7 +2237,13 @@ pub(crate) async fn reconcile_incomplete_runs_on_startup( "Fabro server restarted before the run reached a terminal state.".to_string(), ); let failure_event = workflow_event::Event::workflow_run_failed_from_error( - &error, 0, reason, None, None, None, None, + &error, + fabro_types::RunTiming::default(), + reason, + None, + None, + None, + None, ); workflow_event::append_event(&run_store, &summary.id, &failure_event).await?; reconciled += 1; @@ -2282,7 +2288,13 @@ async fn persist_shutdown_run_failures( "Fabro server shut down before the run reached a terminal state.".to_string(), ); let failure_event = workflow_event::Event::workflow_run_failed_from_error( - &error, 0, reason, None, None, None, None, + &error, + fabro_types::RunTiming::default(), + reason, + None, + None, + None, + None, ); workflow_event::append_event(&run_store, &run_id, &failure_event).await?; } @@ -2354,7 +2366,7 @@ async fn persist_cancelled_run_status(state: &AppState, run_id: RunId) -> anyhow let failure_event = workflow_event::Event::workflow_run_failed_from_error( &WorkflowError::Cancelled, - 0, + fabro_types::RunTiming::default(), FailureReason::Cancelled, None, None, @@ -2390,7 +2402,7 @@ async fn fail_run_before_execution( Ok(run_store) => { let failure_event = workflow_event::Event::workflow_run_failed_from_error( &WorkflowError::engine(message.clone()), - 0, + fabro_types::RunTiming::default(), reason, None, None, @@ -2704,7 +2716,13 @@ async fn append_worker_exit_failure( format!("Worker exited before emitting a terminal run event: {wait_status}"), ); let failure_event = workflow_event::Event::workflow_run_failed_from_error( - &error, 0, reason, None, None, None, None, + &error, + fabro_types::RunTiming::default(), + reason, + None, + None, + None, + None, ); if let Err(err) = workflow_event::append_event(run_store, &run_id, &failure_event).await { @@ -3365,7 +3383,7 @@ async fn execute_run_subprocess(state: Arc, run_id: RunId) { let message = format!("Failed to spawn worker: {err}"); let failure_event = workflow_event::Event::workflow_run_failed_from_error( &WorkflowError::engine_with_anyhow("Failed to spawn worker", err), - 0, + fabro_types::RunTiming::default(), FailureReason::LaunchFailed, None, None, @@ -3385,7 +3403,7 @@ async fn execute_run_subprocess(state: Arc, run_id: RunId) { let _ = child.start_kill(); let failure_event = workflow_event::Event::workflow_run_failed_from_error( &WorkflowError::engine(message.clone()), - 0, + fabro_types::RunTiming::default(), FailureReason::LaunchFailed, None, None, @@ -3413,7 +3431,7 @@ async fn execute_run_subprocess(state: Arc, run_id: RunId) { let _ = child.start_kill(); let failure_event = workflow_event::Event::workflow_run_failed_from_error( &WorkflowError::engine(message.clone()), - 0, + fabro_types::RunTiming::default(), FailureReason::LaunchFailed, None, None, @@ -3432,7 +3450,7 @@ async fn execute_run_subprocess(state: Arc, run_id: RunId) { let _ = child.start_kill(); let failure_event = workflow_event::Event::workflow_run_failed_from_error( &WorkflowError::engine(message.clone()), - 0, + fabro_types::RunTiming::default(), FailureReason::LaunchFailed, None, None, @@ -3464,7 +3482,7 @@ async fn execute_run_subprocess(state: Arc, run_id: RunId) { let _ = child.start_kill(); let failure_event = workflow_event::Event::workflow_run_failed_from_error( &WorkflowError::engine_with_source("Worker wait failed", err), - 0, + fabro_types::RunTiming::default(), FailureReason::Terminated, None, None, diff --git a/lib/crates/fabro-server/src/server/handler/billing.rs b/lib/crates/fabro-server/src/server/handler/billing.rs index cbaf7cc3e..c384287cd 100644 --- a/lib/crates/fabro-server/src/server/handler/billing.rs +++ b/lib/crates/fabro-server/src/server/handler/billing.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use std::sync::Arc; use chrono::{DateTime, Utc}; -use fabro_types::{RunProjection, StageHandler, StageProjection, StageState}; +use fabro_types::{RunProjection, StageHandler, StageProjection, StageState, StageTiming}; use super::super::{ ApiError, AppState, BillingByModel, BillingStageRef, IntoResponse, Json, ListResponse, @@ -54,7 +54,7 @@ async fn list_run_stages( stage_id, stage_id.node_id().to_string(), stage.effective_state(), - stage.runtime_secs(now), + stage.live_wall_time_ms(now), stage.started_at, handler, ) @@ -97,23 +97,25 @@ async fn get_run_billing( .map(|stage| (stage.node_id.as_str(), stage)) .collect::>(); let live_rows = live_billing_rows(&projection, Utc::now()); - let runtime_secs = live_rows.iter().map(|row| row.runtime_secs).sum::(); + let totals_timing = live_rows.iter().fold(StageTiming::default(), |acc, row| { + acc.saturating_add(&row.timing) + }); let stages = live_rows .into_iter() .map(|row| { let rollup_stage = rollup_by_node.get(row.node_id.as_str()); RunBillingStage { - billing: rollup_stage + billing: rollup_stage .map(|stage| stage.billing.clone()) .unwrap_or_default(), - model: rollup_stage.and_then(|stage| stage.model.as_ref()).cloned(), - runtime_secs: row.runtime_secs, - stage: BillingStageRef { + model: rollup_stage.and_then(|stage| stage.model.as_ref()).cloned(), + timing: row.timing, + stage: BillingStageRef { id: row.node_id.clone(), name: row.node_id, }, - started_at: row.started_at, - state: row.state, + started_at: row.started_at, + state: row.state, } }) .collect::>(); @@ -122,14 +124,14 @@ async fn get_run_billing( by_model, stages, totals: RunBillingTotals { - cache_read_tokens: rollup.totals.cache_read_tokens, + cache_read_tokens: rollup.totals.cache_read_tokens, cache_write_tokens: rollup.totals.cache_write_tokens, - input_tokens: rollup.totals.input_tokens, - output_tokens: rollup.totals.output_tokens, - reasoning_tokens: rollup.totals.reasoning_tokens, - runtime_secs, - total_tokens: rollup.totals.total_tokens, - total_usd_micros: rollup.totals.total_usd_micros, + input_tokens: rollup.totals.input_tokens, + output_tokens: rollup.totals.output_tokens, + reasoning_tokens: rollup.totals.reasoning_tokens, + timing: totals_timing.into(), + total_tokens: rollup.totals.total_tokens, + total_usd_micros: rollup.totals.total_usd_micros, }, }; @@ -138,7 +140,7 @@ async fn get_run_billing( struct LiveBillingRow { node_id: String, - runtime_secs: f64, + timing: StageTiming, started_at: Option>, state: Option, latest_visit: u32, @@ -158,7 +160,7 @@ fn live_billing_rows(projection: &RunProjection, now: DateTime) -> Vec) -> Vec= row.latest_visit { row.latest_visit = stage_id.visit(); @@ -178,16 +181,23 @@ fn live_billing_rows(projection: &RunProjection, now: DateTime) -> Vec) -> Option { - stage - .duration_ms - .map(|ms| ms as f64 / 1000.0) - .or_else(|| stage.runtime_secs(now)) +/// Per-visit timing for a stage. For terminal visits, the stored breakdown is +/// used directly. For in-flight visits, fall back to the live wall-clock since +/// `started_at` (no active breakdown yet — that is only finalized at terminal +/// event time in v1). +fn billing_stage_timing(stage: &StageProjection, now: DateTime) -> StageTiming { + if let Some(timing) = stage.timing { + return timing; + } + if let Some(live_wall) = stage.live_wall_time_ms(now) { + return StageTiming::wall_only(live_wall); + } + StageTiming::default() } fn stage_has_billing_row(stage: &StageProjection) -> bool { stage.completion.is_some() - || stage.duration_ms.is_some() + || stage.timing.is_some() || !stage.usage.is_zero() || stage.started_at.is_some() } diff --git a/lib/crates/fabro-server/src/server/handler/system.rs b/lib/crates/fabro-server/src/server/handler/system.rs index fcc279119..adb25634c 100644 --- a/lib/crates/fabro-server/src/server/handler/system.rs +++ b/lib/crates/fabro-server/src/server/handler/system.rs @@ -541,7 +541,7 @@ async fn get_aggregate_billing( output_tokens: total_billing.output_tokens, reasoning_tokens: total_billing.reasoning_tokens, runs: agg.total_runs, - runtime_secs: agg.total_runtime_secs, + timing: agg.total_timing, total_tokens: total_billing.total_tokens, total_usd_micros: total_billing.total_usd_micros, }, diff --git a/lib/crates/fabro-server/src/server/tests.rs b/lib/crates/fabro-server/src/server/tests.rs index 52affb51e..3736b02ca 100644 --- a/lib/crates/fabro-server/src/server/tests.rs +++ b/lib/crates/fabro-server/src/server/tests.rs @@ -2743,7 +2743,7 @@ async fn persist_cancelled_run_status_ignores_already_terminal_runs() { let run_id = fixtures::RUN_1; create_durable_run_with_events(&state, run_id, &[ workflow_event::Event::WorkflowRunCompleted { - duration_ms: 1000, + timing: fabro_types::RunTiming::wall_only(1000), artifact_count: 0, status: "succeeded".to_string(), reason: SuccessReason::Completed, @@ -2779,7 +2779,7 @@ async fn delete_terminal_managed_run_does_not_send_cancel_signal() { let run_id = fixtures::RUN_1; create_durable_run_with_events(&state, run_id, &[ workflow_event::Event::WorkflowRunCompleted { - duration_ms: 1000, + timing: fabro_types::RunTiming::wall_only(1000), artifact_count: 0, status: "succeeded".to_string(), reason: SuccessReason::Completed, @@ -2889,7 +2889,7 @@ async fn list_run_stages_projects_retrying_until_completion() { node_id: "setup".to_string(), name: "Setup".to_string(), index: 0, - duration_ms: 5, + timing: fabro_types::StageTiming::wall_only(5), status: "succeeded".to_string(), preferred_label: None, suggested_next_ids: Vec::new(), @@ -2930,14 +2930,14 @@ async fn list_run_stages_projects_retrying_until_completion() { "work", 1, &workflow_event::Event::StageFailed { - node_id: "work".to_string(), - name: "Work".to_string(), - index: 1, - failure: FailureDetail::new("try again", FailureCategory::TransientInfra), - will_retry: true, - duration_ms: 10, - billing: None, - actor: None, + node_id: "work".to_string(), + name: "Work".to_string(), + index: 1, + failure: FailureDetail::new("try again", FailureCategory::TransientInfra), + will_retry: true, + timing: fabro_types::StageTiming::wall_only(10), + billing: None, + actor: None, }, ) .await; @@ -2981,7 +2981,7 @@ async fn list_run_stages_projects_retrying_until_completion() { node_id: "work".to_string(), name: "Work".to_string(), index: 1, - duration_ms: 25, + timing: fabro_types::StageTiming::wall_only(25), status: "partially_succeeded".to_string(), preferred_label: None, suggested_next_ids: Vec::new(), @@ -3111,7 +3111,7 @@ async fn list_run_stages_distinguishes_visits() { node_id: "verify".to_string(), name: "Verify".to_string(), index: 1, - duration_ms: 1500, + timing: fabro_types::StageTiming::wall_only(1500), status: "failed".to_string(), preferred_label: None, suggested_next_ids: Vec::new(), @@ -3171,7 +3171,7 @@ async fn list_run_stages_distinguishes_visits() { assert_eq!(first["visit"], 1); assert_eq!(first["handler"], "command"); assert_eq!(first["status"], "failed"); - assert_eq!(first["duration_secs"], 1.5); + assert_eq!(first["wall_time_ms"], 1500); let second = stage_entry(&body, "verify@2"); assert_eq!(second["node_id"], "verify"); @@ -3211,7 +3211,7 @@ async fn run_billing_dedups_retried_nodes_and_sums_their_durations() { node_id: "verify".to_string(), name: "Verify".to_string(), index: 1, - duration_ms: 1500, + timing: fabro_types::StageTiming::wall_only(1500), status: "failed".to_string(), preferred_label: None, suggested_next_ids: Vec::new(), @@ -3242,7 +3242,7 @@ async fn run_billing_dedups_retried_nodes_and_sums_their_durations() { node_id: "verify".to_string(), name: "Verify".to_string(), index: 1, - duration_ms: 800, + timing: fabro_types::StageTiming::wall_only(800), status: "succeeded".to_string(), preferred_label: None, suggested_next_ids: Vec::new(), @@ -3314,16 +3314,16 @@ async fn run_billing_dedups_retried_nodes_and_sums_their_durations() { assert_eq!(stages[0]["stage"]["id"], "verify"); // Duration on the row is the sum across visits (1.5s + 0.8s = 2.3s). assert!( - (stages[0]["runtime_secs"].as_f64().unwrap() - 2.3).abs() < f64::EPSILON, + stages[0]["timing"]["wall_time_ms"].as_u64().unwrap() == 2300, "row runtime_secs should sum visits, got {}", - stages[0]["runtime_secs"] + stages[0]["timing"]["wall_time_ms"] ); // Totals must not double-count: a single 2.3s, not 4.6s. assert!( - (body["totals"]["runtime_secs"].as_f64().unwrap() - 2.3).abs() < f64::EPSILON, + body["totals"]["timing"]["wall_time_ms"].as_u64().unwrap() == 2300, "totals.runtime_secs should sum visits exactly once, got {}", - body["totals"]["runtime_secs"] + body["totals"]["timing"]["wall_time_ms"] ); } @@ -3350,14 +3350,14 @@ async fn run_billing_sums_usage_across_retry_visits_and_uses_latest_model() { "verify", 1, &workflow_event::Event::StageFailed { - node_id: "verify".to_string(), - name: "Verify".to_string(), - index: 1, - failure: FailureDetail::new("try again", FailureCategory::TransientInfra), - will_retry: true, - duration_ms: 1200, - billing: Some(failed_usage), - actor: None, + node_id: "verify".to_string(), + name: "Verify".to_string(), + index: 1, + failure: FailureDetail::new("try again", FailureCategory::TransientInfra), + will_retry: true, + timing: fabro_types::StageTiming::wall_only(1200), + billing: Some(failed_usage), + actor: None, }, ) .await; @@ -3370,7 +3370,7 @@ async fn run_billing_sums_usage_across_retry_visits_and_uses_latest_model() { node_id: "verify".to_string(), name: "Verify".to_string(), index: 1, - duration_ms: 800, + timing: fabro_types::StageTiming::wall_only(800), status: "succeeded".to_string(), preferred_label: None, suggested_next_ids: Vec::new(), @@ -3393,7 +3393,7 @@ async fn run_billing_sums_usage_across_retry_visits_and_uses_latest_model() { let mut latest_outcome: Outcome> = Outcome::success(); latest_outcome.usage = Some(success_usage); - latest_outcome.duration_ms = Some(800); + latest_outcome.timing = Some(fabro_types::StageTiming::wall_only(800)); let run_store = state.store.open_run(&run_id).await.unwrap(); workflow_event::append_event( &run_store, @@ -3442,12 +3442,12 @@ async fn run_billing_sums_usage_across_retry_visits_and_uses_latest_model() { assert_eq!(stages[0]["billing"]["input_tokens"], 300); assert_eq!(stages[0]["billing"]["output_tokens"], 30); assert_eq!(stages[0]["billing"]["total_usd_micros"], 330); - assert!((stages[0]["runtime_secs"].as_f64().unwrap() - 2.0).abs() < f64::EPSILON); + assert!(stages[0]["timing"]["wall_time_ms"].as_u64().unwrap() == 2000); assert_eq!(body["totals"]["input_tokens"], 300); assert_eq!(body["totals"]["output_tokens"], 30); assert_eq!(body["totals"]["total_usd_micros"], 330); - assert!((body["totals"]["runtime_secs"].as_f64().unwrap() - 2.0).abs() < f64::EPSILON); + assert!(body["totals"]["timing"]["wall_time_ms"].as_u64().unwrap() == 2000); let by_model = body["by_model"].as_array().unwrap(); assert_eq!(by_model.len(), 2); @@ -3503,14 +3503,14 @@ async fn list_run_stages_shows_retrying_after_failed_event() { "work", 1, &workflow_event::Event::StageFailed { - node_id: "work".to_string(), - name: "Work".to_string(), - index: 0, - failure: FailureDetail::new("flake", FailureCategory::TransientInfra), - will_retry: true, - duration_ms: 5, - billing: None, - actor: None, + node_id: "work".to_string(), + name: "Work".to_string(), + index: 0, + failure: FailureDetail::new("flake", FailureCategory::TransientInfra), + will_retry: true, + timing: fabro_types::StageTiming::wall_only(5), + billing: None, + actor: None, }, ) .await; @@ -3583,14 +3583,14 @@ async fn list_run_stages_shows_retrying_when_failed_will_retry() { "work", 1, &workflow_event::Event::StageFailed { - node_id: "work".to_string(), - name: "Work".to_string(), - index: 0, - failure: FailureDetail::new("flake", FailureCategory::TransientInfra), - will_retry: true, - duration_ms: 5, - billing: None, - actor: None, + node_id: "work".to_string(), + name: "Work".to_string(), + index: 0, + failure: FailureDetail::new("flake", FailureCategory::TransientInfra), + will_retry: true, + timing: fabro_types::StageTiming::wall_only(5), + billing: None, + actor: None, }, ) .await; @@ -3631,14 +3631,14 @@ async fn run_billing_retried_node_then_succeeded_emits_one_row_with_final_attemp max_attempts: 3, }, workflow_event::Event::StageFailed { - node_id: "work".to_string(), - name: "Work".to_string(), - index: 0, - failure: FailureDetail::new("transient", FailureCategory::TransientInfra), - will_retry: true, - duration_ms: 10, - billing: None, - actor: None, + node_id: "work".to_string(), + name: "Work".to_string(), + index: 0, + failure: FailureDetail::new("transient", FailureCategory::TransientInfra), + will_retry: true, + timing: fabro_types::StageTiming::wall_only(10), + billing: None, + actor: None, }, workflow_event::Event::StageRetrying { node_id: "work".to_string(), @@ -3660,7 +3660,7 @@ async fn run_billing_retried_node_then_succeeded_emits_one_row_with_final_attemp node_id: "work".to_string(), name: "Work".to_string(), index: 0, - duration_ms: 25, + timing: fabro_types::StageTiming::wall_only(25), status: "succeeded".to_string(), preferred_label: None, suggested_next_ids: Vec::new(), @@ -3700,9 +3700,9 @@ async fn run_billing_retried_node_then_succeeded_emits_one_row_with_final_attemp row["state"], "succeeded", "final state mirrors the latest StageCompleted" ); - let runtime = row["runtime_secs"].as_f64().unwrap(); - assert!( - (runtime - 0.025).abs() < f64::EPSILON, + let runtime = row["timing"]["wall_time_ms"].as_u64().unwrap(); + assert_eq!( + runtime, 25, "runtime should equal final attempt's 25ms, got {runtime}" ); } @@ -3729,7 +3729,7 @@ fn revisit_test_completed_with_visit( node_id: node_id.to_string(), name: node_id.to_string(), index: 0, - duration_ms, + timing: fabro_types::StageTiming::wall_only(duration_ms), status: "succeeded".to_string(), preferred_label: None, suggested_next_ids: Vec::new(), @@ -3790,14 +3790,14 @@ async fn run_billing_revisited_node_collapses_to_two_rows_with_summed_visit_dura "A appeared first → A's row first" ); assert_eq!(stages[1]["stage"]["id"], "b"); - let a_runtime = stages[0]["runtime_secs"].as_f64().unwrap(); - assert!( - (a_runtime - 0.1).abs() < f64::EPSILON, + let a_runtime = stages[0]["timing"]["wall_time_ms"].as_u64().unwrap(); + assert_eq!( + a_runtime, 100, "A should sum both visit durations (1ms + 99ms), got {a_runtime}" ); - let b_runtime = stages[1]["runtime_secs"].as_f64().unwrap(); - assert!( - (b_runtime - 0.002).abs() < f64::EPSILON, + let b_runtime = stages[1]["timing"]["wall_time_ms"].as_u64().unwrap(); + assert_eq!( + b_runtime, 2, "B should carry its single visit's duration (2ms), got {b_runtime}" ); } @@ -3843,7 +3843,12 @@ async fn create_unreadable_durable_run(state: &Arc, run_id: RunId) { "run_id": run_id, "event": "run.completed", "properties": { - "duration_ms": 1, + "timing": { + "wall_time_ms": 1, + "inference_time_ms": 0, + "tool_time_ms": 0, + "active_time_ms": 0 + }, "artifact_count": 0, "status": "legacy-status", "reason": "completed", @@ -4086,7 +4091,7 @@ async fn create_completed_run_ready_for_pull_request( goal: Some("Ship the server-side PR".to_string()), }, workflow_event::Event::WorkflowRunCompleted { - duration_ms: 1, + timing: fabro_types::RunTiming::wall_only(1), artifact_count: 0, status: "succeeded".to_string(), reason: SuccessReason::Completed, @@ -7785,7 +7790,7 @@ async fn patch_run_title_updates_active_and_archived_runs() { &run_store, &run_id, &workflow_event::Event::WorkflowRunCompleted { - duration_ms: 1, + timing: fabro_types::RunTiming::wall_only(1), artifact_count: 0, status: "succeeded".to_string(), reason: SuccessReason::Completed, @@ -7933,7 +7938,7 @@ async fn cancel_terminal_durable_run_returns_conflict() { let run_id = fixtures::RUN_1; create_durable_run_with_events(&state, run_id, &[ workflow_event::Event::WorkflowRunCompleted { - duration_ms: 1000, + timing: fabro_types::RunTiming::wall_only(1000), artifact_count: 0, status: "succeeded".to_string(), reason: SuccessReason::Completed, @@ -7983,7 +7988,7 @@ async fn steer_terminal_durable_run_returns_run_not_steerable() { let run_id = fixtures::RUN_1; create_durable_run_with_events(&state, run_id, &[ workflow_event::Event::WorkflowRunCompleted { - duration_ms: 1000, + timing: fabro_types::RunTiming::wall_only(1000), artifact_count: 0, status: "succeeded".to_string(), reason: SuccessReason::Completed, @@ -8402,7 +8407,7 @@ async fn active_acp_steerable_marker_clears_on_terminal_paths() { node_id: "agent".to_string(), name: "agent".to_string(), index: 0, - duration_ms: 1, + timing: fabro_types::StageTiming::wall_only(1), status: "success".to_string(), preferred_label: None, suggested_next_ids: Vec::new(), @@ -8421,14 +8426,14 @@ async fn active_acp_steerable_marker_clears_on_terminal_paths() { max_attempts: 1, }, workflow_event::Event::StageFailed { - node_id: "agent".to_string(), - name: "agent".to_string(), - index: 0, - failure: FailureDetail::new("failed", FailureCategory::Deterministic), - will_retry: false, - duration_ms: 1, - billing: None, - actor: None, + node_id: "agent".to_string(), + name: "agent".to_string(), + index: 0, + failure: FailureDetail::new("failed", FailureCategory::Deterministic), + will_retry: false, + timing: fabro_types::StageTiming::wall_only(1), + billing: None, + actor: None, }, ]; @@ -8837,7 +8842,7 @@ async fn archive_and_unarchive_updates_listing_visibility() { workflow_event::Event::RunStarting, workflow_event::Event::RunRunning, workflow_event::Event::WorkflowRunCompleted { - duration_ms: 1000, + timing: fabro_types::RunTiming::wall_only(1000), artifact_count: 0, status: "succeeded".to_string(), reason: SuccessReason::Completed, @@ -9171,7 +9176,7 @@ async fn delete_run_retry_after_missing_provider_resource_removes_metadata() { primary_repo_link: None, }, workflow_event::Event::WorkflowRunCompleted { - duration_ms: 1, + timing: fabro_types::RunTiming::wall_only(1), artifact_count: 0, status: "succeeded".to_string(), reason: SuccessReason::Completed, @@ -9299,7 +9304,10 @@ async fn get_aggregate_billing_returns_zeros_initially() { assert_eq!(body["totals"]["runs"].as_i64().unwrap(), 0); assert_eq!(body["totals"]["input_tokens"].as_i64().unwrap(), 0); assert_eq!(body["totals"]["output_tokens"].as_i64().unwrap(), 0); - assert_eq!(body["totals"]["runtime_secs"].as_f64().unwrap(), 0.0); + assert_eq!( + body["totals"]["timing"]["wall_time_ms"].as_u64().unwrap(), + 0 + ); assert!(body["totals"]["total_usd_micros"].is_null()); assert!(body["by_model"].as_array().unwrap().is_empty()); } @@ -9434,14 +9442,14 @@ fn aggregate_billing_counts_projection_rollup_usage_visits() { }, }, ], - runtime_ms: 2000, + timing: fabro_types::RunTiming::wall_only(2000), billed_visit_count: 2, }; accumulate_billing_rollup(&mut accumulator, &rollup); assert_eq!(accumulator.total_runs, 1); - assert_eq!(accumulator.total_runtime_secs, 2.0); + assert_eq!(accumulator.total_timing.wall_time_ms, 2000); assert_eq!(accumulator.by_model.len(), 2); assert_eq!( accumulator.by_model[&ModelRef { @@ -10671,7 +10679,7 @@ async fn boards_runs_excludes_archived_by_default() { workflow_event::Event::RunStarting, workflow_event::Event::RunRunning, workflow_event::Event::WorkflowRunCompleted { - duration_ms: 1000, + timing: fabro_types::RunTiming::wall_only(1000), artifact_count: 0, status: "succeeded".to_string(), reason: SuccessReason::Completed, @@ -10720,7 +10728,7 @@ async fn boards_runs_includes_archived_when_flag_set() { workflow_event::Event::RunStarting, workflow_event::Event::RunRunning, workflow_event::Event::WorkflowRunCompleted { - duration_ms: 1000, + timing: fabro_types::RunTiming::wall_only(1000), artifact_count: 0, status: "succeeded".to_string(), reason: SuccessReason::Completed, @@ -10740,7 +10748,7 @@ async fn boards_runs_includes_archived_when_flag_set() { workflow_event::Event::RunStarting, workflow_event::Event::RunRunning, workflow_event::Event::WorkflowRunCompleted { - duration_ms: 1000, + timing: fabro_types::RunTiming::wall_only(1000), artifact_count: 0, status: "succeeded".to_string(), reason: SuccessReason::Completed, @@ -10813,7 +10821,7 @@ async fn get_run_exposes_canonical_operator_statuses() { workflow_event::Event::RunStarting, workflow_event::Event::RunRunning, workflow_event::Event::WorkflowRunCompleted { - duration_ms: 1000, + timing: fabro_types::RunTiming::wall_only(1000), artifact_count: 0, status: "succeeded".to_string(), reason: SuccessReason::Completed, @@ -10898,7 +10906,7 @@ async fn boards_runs_maps_statuses_to_columns() { workflow_event::Event::RunStarting, workflow_event::Event::RunRunning, workflow_event::Event::WorkflowRunCompleted { - duration_ms: 1000, + timing: fabro_types::RunTiming::wall_only(1000), artifact_count: 0, status: "succeeded".to_string(), reason: SuccessReason::Completed, diff --git a/lib/crates/fabro-server/tests/it/api/run_files.rs b/lib/crates/fabro-server/tests/it/api/run_files.rs index b70688252..88df727c8 100644 --- a/lib/crates/fabro-server/tests/it/api/run_files.rs +++ b/lib/crates/fabro-server/tests/it/api/run_files.rs @@ -102,7 +102,7 @@ async fn append_completed_run_with_final_patch( &run_store, run_id, &workflow_event::Event::WorkflowRunCompleted { - duration_ms: 1, + timing: fabro_types::RunTiming::wall_only(1), artifact_count: 0, status: "succeeded".to_string(), reason: SuccessReason::Completed, diff --git a/lib/crates/fabro-server/tests/it/scenario/usage.rs b/lib/crates/fabro-server/tests/it/scenario/usage.rs index db5959e3c..7215e101c 100644 --- a/lib/crates/fabro-server/tests/it/scenario/usage.rs +++ b/lib/crates/fabro-server/tests/it/scenario/usage.rs @@ -133,12 +133,12 @@ fn assert_non_llm_billing(billing: &serde_json::Value, expected_stage_ids: &[&st "every non-LLM stage should have null model and zero token counts: {stages:?}" ); - let runtime_secs: f64 = stages + let stage_wall_sum: u64 = stages .iter() .map(|stage| { - stage["runtime_secs"] - .as_f64() - .expect("stage should include runtime_secs") + stage["timing"]["wall_time_ms"] + .as_u64() + .expect("stage should include timing.wall_time_ms") }) .sum(); @@ -153,11 +153,11 @@ fn assert_non_llm_billing(billing: &serde_json::Value, expected_stage_ids: &[&st assert_eq!(billing["totals"]["output_tokens"], 0); assert!(billing["totals"]["total_usd_micros"].is_null()); - let total_runtime_secs = billing["totals"]["runtime_secs"] - .as_f64() - .expect("totals should include runtime_secs"); - assert!( - (total_runtime_secs - runtime_secs).abs() < f64::EPSILON, - "total runtime {total_runtime_secs} should equal summed stage runtime {runtime_secs}" + let total_wall_time_ms = billing["totals"]["timing"]["wall_time_ms"] + .as_u64() + .expect("totals should include timing.wall_time_ms"); + assert_eq!( + total_wall_time_ms, stage_wall_sum, + "total wall_time_ms {total_wall_time_ms} should equal summed stage wall_time_ms {stage_wall_sum}" ); } diff --git a/lib/crates/fabro-store/src/run_state.rs b/lib/crates/fabro-store/src/run_state.rs index bf253c119..c9916f64b 100644 --- a/lib/crates/fabro-store/src/run_state.rs +++ b/lib/crates/fabro-store/src/run_state.rs @@ -333,7 +333,7 @@ impl RunProjectionReducer for RunProjection { }; stage.response = response; stage.completion = Some(completion); - stage.duration_ms = Some(props.duration_ms); + stage.timing = Some(props.timing); if let Some(billing) = &props.billing { stage.usage.replace_with_billed_usage(billing); stage.model = Some(billing.model().clone()); @@ -354,7 +354,7 @@ impl RunProjectionReducer for RunProjection { failure_reason, timestamp: ts, }); - stage.duration_ms = Some(props.duration_ms); + stage.timing = Some(props.timing); if let Some(billing) = &props.billing { stage.usage.replace_with_billed_usage(billing); stage.model = Some(billing.model().clone()); @@ -620,10 +620,10 @@ pub(crate) fn build_summary(state: &RunProjection, run_id: &RunId) -> Run { .conclusion .as_ref() .map(|conclusion| conclusion.timestamp); - let duration_ms = state + let run_timing = state .conclusion .as_ref() - .map(|conclusion| conclusion.duration_ms); + .map(|conclusion| conclusion.timing); let total_usd_micros = state .conclusion .as_ref() @@ -669,9 +669,8 @@ pub(crate) fn build_summary(state: &RunProjection, run_id: &RunId) -> Run { started_at: start_time, last_event_at: Some(state.last_event_at), completed_at, - duration_ms, - elapsed_secs: elapsed_secs(duration_ms), }, + timing: run_timing, billing: total_usd_micros.map(|total_usd_micros| RunBillingSummary { total_usd_micros: Some(total_usd_micros), }), @@ -703,10 +702,6 @@ fn run_models(state: &RunProjection) -> Vec { models } -fn elapsed_secs(duration_ms: Option) -> Option { - duration_ms.map(|ms| ms as f64 / 1000.0) -} - fn checkpoint_from_props(props: &CheckpointCompletedProps, timestamp: DateTime) -> Checkpoint { let loop_failure_signatures = props .loop_failure_signatures @@ -751,7 +746,7 @@ fn conclusion_from_completed( timestamp, status: StageOutcome::from_str(&props.status) .map_err(|err| Error::InvalidEvent(format!("invalid completed stage status: {err}")))?, - duration_ms: props.duration_ms, + timing: props.timing, failure: None, final_git_commit_sha: props.final_git_commit_sha.clone(), stages: Vec::new(), @@ -770,7 +765,7 @@ fn conclusion_from_failed(props: &RunFailedProps, timestamp: DateTime) -> C status: StageOutcome::Failed { retry_requested: false, }, - duration_ms: props.duration_ms, + timing: props.timing, failure: Some(props.failure.clone()), final_git_commit_sha: props.final_git_commit_sha.clone(), stages: Vec::new(), @@ -811,7 +806,7 @@ fn stage_outcome_from_props(props: &StageCompletedProps) -> Outcome StageCompletedProps { StageCompletedProps { index: 0, - duration_ms, + timing: fabro_types::StageTiming::wall_only(duration_ms), status, preferred_label: None, suggested_next_ids: Vec::new(), @@ -3015,7 +3010,7 @@ mod tests { .unwrap(); let stage = state.stage(&stage_id).unwrap(); - assert_eq!(stage.duration_ms, Some(42)); + assert_eq!(stage.timing.map(|t| t.wall_time_ms), Some(42)); assert_eq!(stage.usage, usage_counts(&usage)); assert_eq!(stage.model.as_ref(), Some(usage.model())); assert_eq!(stage.state, StageState::Succeeded); @@ -3043,7 +3038,7 @@ mod tests { .unwrap(); let stage = state.stage(&stage_id).unwrap(); - assert_eq!(stage.duration_ms, Some(10)); + assert_eq!(stage.timing.map(|t| t.wall_time_ms), Some(10)); assert_eq!(stage.state, StageState::Failed); } @@ -3116,6 +3111,6 @@ mod tests { assert_eq!(stage.state, StageState::Running); // Prior attempt's terminal data must not leak into the new attempt. assert!(stage.completion.is_none()); - assert_eq!(stage.duration_ms, None); + assert_eq!(stage.timing.map(|t| t.wall_time_ms), None); } } diff --git a/lib/crates/fabro-store/src/slate/mod.rs b/lib/crates/fabro-store/src/slate/mod.rs index 2b9cfc98a..2b0376a85 100644 --- a/lib/crates/fabro-store/src/slate/mod.rs +++ b/lib/crates/fabro-store/src/slate/mod.rs @@ -619,7 +619,7 @@ mod tests { "2026-03-27T12:00:03Z", "run.completed", &serde_json::json!({ - "duration_ms": 3210, + "timing": {"wall_time_ms": 3210, "inference_time_ms": 0, "tool_time_ms": 0, "active_time_ms": 0}, "artifact_count": 1, "status": "succeeded", "reason": "completed", @@ -993,7 +993,7 @@ mod tests { "category": "canceled" } }, - "duration_ms": 1, + "timing": {"wall_time_ms": 1, "inference_time_ms": 0, "tool_time_ms": 0, "active_time_ms": 0}, }), )) .await @@ -1038,7 +1038,7 @@ mod tests { "2026-03-27T12:00:04Z", "run.completed", &serde_json::json!({ - "duration_ms": 3210, + "timing": {"wall_time_ms": 3210, "inference_time_ms": 0, "tool_time_ms": 0, "active_time_ms": 0}, "artifact_count": 1, "status": "succeeded", "reason": "completed", @@ -1397,7 +1397,7 @@ mod tests { "category": "deterministic" } }, - "duration_ms": 1, + "timing": {"wall_time_ms": 1, "inference_time_ms": 0, "tool_time_ms": 0, "active_time_ms": 0}, }), )) .await diff --git a/lib/crates/fabro-store/tests/serializable_projection.rs b/lib/crates/fabro-store/tests/serializable_projection.rs index a580a74f7..eb8e9bd4d 100644 --- a/lib/crates/fabro-store/tests/serializable_projection.rs +++ b/lib/crates/fabro-store/tests/serializable_projection.rs @@ -131,7 +131,7 @@ fn serializable_projection_round_trips_and_trims_bulky_node_fields() { stage.script_invocation = Some(json!({ "command": "cargo test" })); stage.script_timing = Some(json!({ "duration_ms": 10 })); stage.parallel_results = Some(json!([{ "stage": "fanout@1" }])); - stage.duration_ms = Some(1234); + stage.timing = Some(fabro_types::StageTiming::wall_only(1234)); let usage = sample_usage(); let usage_counts = BilledTokenCounts::from_billed_usage(std::slice::from_ref(&usage)); stage.usage = usage_counts.clone(); @@ -186,7 +186,7 @@ fn serializable_projection_round_trips_and_trims_bulky_node_fields() { node.parallel_results, Some(json!([{ "stage": "fanout@1" }])) ); - assert_eq!(node.duration_ms, Some(1234)); + assert_eq!(node.timing.map(|t| t.wall_time_ms), Some(1234)); assert_eq!(node.usage, usage_counts); assert_eq!(node.model.as_ref(), Some(usage.model())); } diff --git a/lib/crates/fabro-tool/src/common.rs b/lib/crates/fabro-tool/src/common.rs index 492d83ee5..d8fca2a94 100644 --- a/lib/crates/fabro-tool/src/common.rs +++ b/lib/crates/fabro-tool/src/common.rs @@ -294,9 +294,8 @@ mod tests { started_at: None, last_event_at: None, completed_at: None, - duration_ms: None, - elapsed_secs: None, }, + timing: None, billing: None, diff: None, pull_request: None, diff --git a/lib/crates/fabro-tool/src/create.rs b/lib/crates/fabro-tool/src/create.rs index b69e95ae7..882b02153 100644 --- a/lib/crates/fabro-tool/src/create.rs +++ b/lib/crates/fabro-tool/src/create.rs @@ -510,9 +510,8 @@ mod tests { started_at: None, last_event_at: None, completed_at: None, - duration_ms: None, - elapsed_secs: None, }, + timing: None, billing: None, diff: None, pull_request: None, diff --git a/lib/crates/fabro-tool/src/search.rs b/lib/crates/fabro-tool/src/search.rs index ee0d72064..7a57a4299 100644 --- a/lib/crates/fabro-tool/src/search.rs +++ b/lib/crates/fabro-tool/src/search.rs @@ -463,9 +463,8 @@ mod tests { started_at: None, last_event_at: None, completed_at: None, - duration_ms: None, - elapsed_secs: None, }, + timing: None, billing: None, diff: None, pull_request: None, diff --git a/lib/crates/fabro-types/src/conclusion.rs b/lib/crates/fabro-types/src/conclusion.rs index 9cced24bb..fbc3eb01f 100644 --- a/lib/crates/fabro-types/src/conclusion.rs +++ b/lib/crates/fabro-types/src/conclusion.rs @@ -2,13 +2,15 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use crate::outcome::StageOutcome; -use crate::{BilledTokenCounts, RunDiff, RunFailure}; +use crate::{BilledTokenCounts, RunDiff, RunFailure, RunTiming, StageTiming}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct StageSummary { pub stage_id: String, pub stage_label: String, - pub duration_ms: u64, + /// Per-node timing summed across every visit of the node within this + /// conclusion. `wall_time_ms` is the sum of visit wall times. + pub timing: StageTiming, #[serde(default, skip_serializing_if = "Option::is_none")] pub billing_usd_micros: Option, pub retries: u32, @@ -18,7 +20,9 @@ pub struct StageSummary { pub struct Conclusion { pub timestamp: DateTime, pub status: StageOutcome, - pub duration_ms: u64, + /// Run-level timing. `wall_time_ms` is the run's clock duration; active + /// fields sum work across stage visits and can exceed wall time. + pub timing: RunTiming, #[serde(default, skip_serializing_if = "Option::is_none")] pub failure: Option, #[serde(default, skip_serializing_if = "Option::is_none")] diff --git a/lib/crates/fabro-types/src/event_envelope.rs b/lib/crates/fabro-types/src/event_envelope.rs index b36b176cd..59f1ba21a 100644 --- a/lib/crates/fabro-types/src/event_envelope.rs +++ b/lib/crates/fabro-types/src/event_envelope.rs @@ -16,7 +16,8 @@ mod tests { use super::EventEnvelope; use crate::run_event::RunCompletedProps; use crate::{ - EventBody, ParallelBranchId, Principal, RunEvent, StageId, SuccessReason, fixtures, + EventBody, ParallelBranchId, Principal, RunEvent, RunTiming, StageId, SuccessReason, + fixtures, }; #[test] @@ -35,7 +36,7 @@ mod tests { tool_call_id: None, actor: None, body: EventBody::RunCompleted(RunCompletedProps { - duration_ms: 42, + timing: RunTiming::wall_only(42), artifact_count: 0, status: "success".to_string(), reason: SuccessReason::Completed, @@ -79,7 +80,7 @@ mod tests { model: Some("claude-sonnet".to_string()), }), body: EventBody::RunCompleted(RunCompletedProps { - duration_ms: 100, + timing: RunTiming::wall_only(100), artifact_count: 1, status: "success".to_string(), reason: SuccessReason::Completed, diff --git a/lib/crates/fabro-types/src/lib.rs b/lib/crates/fabro-types/src/lib.rs index e64de3f97..46250fa2a 100644 --- a/lib/crates/fabro-types/src/lib.rs +++ b/lib/crates/fabro-types/src/lib.rs @@ -42,6 +42,7 @@ pub mod stage_id; pub mod start; pub mod status; pub mod steering; +pub mod timing; pub use artifact::ArtifactUpload; pub use auth::{IdpIdentity, IdpIdentityError}; @@ -131,3 +132,4 @@ pub use status::{ TerminalStatus, }; pub use steering::SteeringMessage; +pub use timing::{RunTiming, StageTiming}; diff --git a/lib/crates/fabro-types/src/outcome.rs b/lib/crates/fabro-types/src/outcome.rs index 1c09ed6dd..f522f73e7 100644 --- a/lib/crates/fabro-types/src/outcome.rs +++ b/lib/crates/fabro-types/src/outcome.rs @@ -8,7 +8,7 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde_json::Value; use strum::{Display, EnumString, IntoStaticStr}; -use crate::{ExecOutputTail, FailureSignature, SystemActorKind}; +use crate::{ExecOutputTail, FailureSignature, StageTiming, SystemActorKind}; pub trait OutcomeMeta: Default + Clone + Send + Sync + fmt::Debug + Serialize + DeserializeOwned + 'static @@ -274,8 +274,13 @@ pub struct Outcome { pub usage: M, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub files_touched: Vec, + /// Stage timing breakdown captured by the workflow engine. + /// + /// `None` until the stage produces a terminal outcome with a measured + /// wall time. Stage handlers that perform no inference or tool work + /// populate this with [`StageTiming::wall_only`]. #[serde(default, skip_serializing_if = "Option::is_none")] - pub duration_ms: Option, + pub timing: Option, } impl Default for Outcome { @@ -290,7 +295,7 @@ impl Default for Outcome { failure: None, usage: M::default(), files_touched: Vec::new(), - duration_ms: None, + timing: None, } } } @@ -402,17 +407,35 @@ mod tests { #[derive(Debug, Clone)] pub struct NodeResult { - pub outcome: Outcome, - pub duration: Duration, - pub attempts: u32, - pub max_attempts: u32, + pub outcome: Outcome, + /// Wall-clock time spent executing this node attempt (including handler + /// internal waits). Independent of the `inference_time` / `tool_time` + /// breakdown — those are work-only measurements. + pub wall_time: Duration, + /// Sum of LLM request/stream elapsed time across this node attempt. Zero + /// for non-LLM handlers. + pub inference_time: Duration, + /// Sum of tool/command execution elapsed time across this node attempt. + /// Zero for handlers that do not invoke tools or commands. + pub tool_time: Duration, + pub attempts: u32, + pub max_attempts: u32, } impl NodeResult { - pub fn new(outcome: Outcome, duration: Duration, attempts: u32, max_attempts: u32) -> Self { + pub fn new( + outcome: Outcome, + wall_time: Duration, + inference_time: Duration, + tool_time: Duration, + attempts: u32, + max_attempts: u32, + ) -> Self { Self { outcome, - duration, + wall_time, + inference_time, + tool_time, attempts, max_attempts, } @@ -421,7 +444,9 @@ impl NodeResult { pub fn from_skip(outcome: Outcome) -> Self { Self { outcome, - duration: Duration::ZERO, + wall_time: Duration::ZERO, + inference_time: Duration::ZERO, + tool_time: Duration::ZERO, attempts: 0, max_attempts: 0, } diff --git a/lib/crates/fabro-types/src/run_event/mod.rs b/lib/crates/fabro-types/src/run_event/mod.rs index 680ade98a..27bba053b 100644 --- a/lib/crates/fabro-types/src/run_event/mod.rs +++ b/lib/crates/fabro-types/src/run_event/mod.rs @@ -940,7 +940,7 @@ mod tests { actor: None, body: EventBody::StageCompleted(StageCompletedProps { index: 1, - duration_ms: 1234, + timing: crate::StageTiming::wall_only(1234), status: crate::StageOutcome::Succeeded, preferred_label: None, suggested_next_ids: vec!["next".to_string()], @@ -1135,7 +1135,12 @@ mod tests { ( "run.completed", json!({ - "duration_ms": 42, + "timing": { + "wall_time_ms": 42, + "inference_time_ms": 0, + "tool_time_ms": 0, + "active_time_ms": 0 + }, "artifact_count": 0, "status": "succeeded", "reason": "completed", @@ -1156,7 +1161,12 @@ mod tests { "category": "deterministic" } }, - "duration_ms": 42, + "timing": { + "wall_time_ms": 42, + "inference_time_ms": 0, + "tool_time_ms": 0, + "active_time_ms": 0 + }, "diff_summary": { "files_changed": 2, "additions": 10, @@ -1214,7 +1224,7 @@ mod tests { fn event_body_event_name_matches_wire_name() { let body = EventBody::StageCompleted(StageCompletedProps { index: 1, - duration_ms: 1234, + timing: crate::StageTiming::wall_only(1234), status: crate::StageOutcome::Succeeded, preferred_label: None, suggested_next_ids: vec!["next".to_string()], diff --git a/lib/crates/fabro-types/src/run_event/run.rs b/lib/crates/fabro-types/src/run_event/run.rs index be5671640..e7f72d68f 100644 --- a/lib/crates/fabro-types/src/run_event/run.rs +++ b/lib/crates/fabro-types/src/run_event/run.rs @@ -6,7 +6,7 @@ use super::{BilledTokenCounts, ExecOutputTail, RunNoticeLevel}; use crate::status::{BlockedReason, SuccessReason}; use crate::{ DiffSummary, ForkSourceRef, GitContext, Graph, PairId, PairTarget, RunBlobId, RunControlAction, - RunFailure, RunId, RunProvenance, WorkflowSettings, + RunFailure, RunId, RunProvenance, RunTiming, WorkflowSettings, }; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -183,7 +183,8 @@ pub struct RunUnarchivedProps {} #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct RunCompletedProps { - pub duration_ms: u64, + /// Run wall-clock time, with active timing breakdown for the run rollup. + pub timing: RunTiming, pub artifact_count: usize, pub status: String, pub reason: SuccessReason, @@ -202,7 +203,8 @@ pub struct RunCompletedProps { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct RunFailedProps { pub failure: RunFailure, - pub duration_ms: u64, + /// Run wall-clock time at failure, with active timing breakdown. + pub timing: RunTiming, #[serde(default, skip_serializing_if = "Option::is_none")] pub final_git_commit_sha: Option, #[serde(default, skip_serializing_if = "Option::is_none")] diff --git a/lib/crates/fabro-types/src/run_event/stage.rs b/lib/crates/fabro-types/src/run_event/stage.rs index 188441d6c..a16e792fb 100644 --- a/lib/crates/fabro-types/src/run_event/stage.rs +++ b/lib/crates/fabro-types/src/run_event/stage.rs @@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; use super::ExecOutputTail; -use crate::{BilledModelUsage, DiffSummary, FailureDetail, Outcome, StageOutcome}; +use crate::{BilledModelUsage, DiffSummary, FailureDetail, Outcome, StageOutcome, StageTiming}; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct StageStartedProps { @@ -17,7 +17,8 @@ pub struct StageStartedProps { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct StageCompletedProps { pub index: usize, - pub duration_ms: u64, + /// Per-attempt timing breakdown for this stage visit. + pub timing: StageTiming, pub status: StageOutcome, #[serde(default, skip_serializing_if = "Option::is_none")] pub preferred_label: Option, @@ -51,14 +52,15 @@ pub struct StageCompletedProps { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct StageFailedProps { - pub index: usize, + pub index: usize, #[serde(default, skip_serializing_if = "Option::is_none")] - pub failure: Option, - pub will_retry: bool, + pub failure: Option, + pub will_retry: bool, + /// Per-attempt timing breakdown for this stage visit. #[serde(default)] - pub duration_ms: u64, + pub timing: StageTiming, #[serde(default, skip_serializing_if = "Option::is_none")] - pub billing: Option, + pub billing: Option, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] diff --git a/lib/crates/fabro-types/src/run_projection.rs b/lib/crates/fabro-types/src/run_projection.rs index ef2186ffc..b39844077 100644 --- a/lib/crates/fabro-types/src/run_projection.rs +++ b/lib/crates/fabro-types/src/run_projection.rs @@ -7,7 +7,7 @@ use chrono::{DateTime, Utc}; use crate::{ BilledTokenCounts, Checkpoint, Conclusion, InterviewQuestionRecord, InvalidTransition, ModelRef, PullRequestLink, RunControlAction, RunDiff, RunId, RunSandbox, RunSpec, RunStatus, - StageCompletion, StageHandler, StageId, StageState, StartRecord, + StageCompletion, StageHandler, StageId, StageState, StageTiming, StartRecord, }; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] @@ -71,8 +71,13 @@ pub struct StageProjection { pub started_at: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] pub handler: Option, + /// Per-attempt timing breakdown for the latest terminal attempt. + /// + /// `None` for stages still in flight (`started_at` is set but no terminal + /// event has been observed yet). For live wall-time ticking, the UI uses + /// `started_at`; once terminal this carries the finalized breakdown. #[serde(default, skip_serializing_if = "Option::is_none")] - pub duration_ms: Option, + pub timing: Option, #[serde(default)] pub usage: BilledTokenCounts, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -95,7 +100,7 @@ impl StageProjection { prompt: None, response: None, completion: None, - duration_ms: None, + timing: None, usage: BilledTokenCounts::default(), model: None, provider_used: None, @@ -119,26 +124,27 @@ impl StageProjection { self.state } - /// Live wall-clock runtime in seconds. + /// Live wall-clock time in milliseconds. /// /// While the stage is non-terminal (`Pending`, `Running`, or `Retrying`), /// this returns the elapsed time since `started_at` so the UI can tick - /// client-side. Once terminal, the stored `duration_ms` is returned. This - /// also handles retries safely: a new `StageStarted` resets the state - /// back to `Running` and keeps the live computation correct even if a - /// previous attempt left a stale `duration_ms`. + /// client-side. Once terminal, the stored `timing.wall_time_ms` is + /// returned. This also handles retries safely: a new `StageStarted` resets + /// the state back to `Running` and keeps the live computation correct + /// even if a previous attempt left stale timing. #[must_use] - pub fn runtime_secs(&self, now: DateTime) -> Option { + pub fn live_wall_time_ms(&self, now: DateTime) -> Option { let state = self.effective_state(); if matches!( state, StageState::Running | StageState::Retrying | StageState::Pending ) { return self.started_at.map(|started| { - now.signed_duration_since(started).num_milliseconds().max(0) as f64 / 1000.0 + u64::try_from(now.signed_duration_since(started).num_milliseconds().max(0)) + .unwrap_or(0) }); } - self.duration_ms.map(|ms| ms as f64 / 1000.0) + self.timing.map(|timing| timing.wall_time_ms) } /// Begin a new attempt (or visit) for this stage: clear every diff --git a/lib/crates/fabro-types/src/run_summary.rs b/lib/crates/fabro-types/src/run_summary.rs index 20da806e0..f3e66b7be 100644 --- a/lib/crates/fabro-types/src/run_summary.rs +++ b/lib/crates/fabro-types/src/run_summary.rs @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize}; use crate::{ DiffSummary, InterviewQuestionRecord, Principal, PullRequestLink, RepositoryRef, - RunControlAction, RunId, RunSandbox, RunStatus, + RunControlAction, RunId, RunSandbox, RunStatus, RunTiming, }; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -33,6 +33,10 @@ pub struct Run { #[serde(default)] pub source_directory: Option, pub timestamps: RunTimestamps, + /// Run-level timing rollup. `None` until the run has measurable timing + /// data; populated once a terminal event or partial rollup is available. + #[serde(default)] + pub timing: Option, #[serde(default)] pub billing: Option, #[serde(default)] @@ -123,10 +127,6 @@ pub struct RunTimestamps { pub last_event_at: Option>, #[serde(default)] pub completed_at: Option>, - #[serde(default)] - pub duration_ms: Option, - #[serde(default)] - pub elapsed_secs: Option, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] diff --git a/lib/crates/fabro-types/src/timing.rs b/lib/crates/fabro-types/src/timing.rs new file mode 100644 index 000000000..993511c6a --- /dev/null +++ b/lib/crates/fabro-types/src/timing.rs @@ -0,0 +1,252 @@ +//! Wall and active timing primitives shared across stages and runs. +//! +//! Two value objects: [`StageTiming`] for one stage visit, [`RunTiming`] for a +//! run-level rollup. Both expose the same four fields: +//! +//! - `wall_time_ms`: elapsed clock time from start to finish. +//! - `inference_time_ms`: Fabro-observed LLM request/stream elapsed time. +//! - `tool_time_ms`: tool or command execution elapsed time. +//! - `active_time_ms`: `inference_time_ms + tool_time_ms`. +//! +//! `active_time_ms` is precomputed and serialized so API consumers do not need +//! to redo the addition. Use the `new` constructors to enforce the invariant. +//! +//! For parallel container stages the container reports `active = 0` and the +//! child branches carry their own work timing; run-level active time sums work +//! across stage visits and can exceed run wall time when work runs in parallel. + +use serde::{Deserialize, Serialize}; + +/// Timing breakdown for one stage visit. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct StageTiming { + /// Wall-clock time from stage start to terminal event. + pub wall_time_ms: u64, + /// Fabro-observed LLM request/stream elapsed time. + #[serde(default)] + pub inference_time_ms: u64, + /// Tool or command execution elapsed time. + #[serde(default)] + pub tool_time_ms: u64, + /// `inference_time_ms + tool_time_ms`. + pub active_time_ms: u64, +} + +impl StageTiming { + /// Construct a [`StageTiming`] with `active_time_ms` derived from the + /// breakdown. + #[must_use] + pub fn new(wall_time_ms: u64, inference_time_ms: u64, tool_time_ms: u64) -> Self { + let active_time_ms = inference_time_ms.saturating_add(tool_time_ms); + Self { + wall_time_ms, + inference_time_ms, + tool_time_ms, + active_time_ms, + } + } + + /// Stages with no inference/tool work (human, wait, conditional, fan-in, + /// start, exit, parallel container) report wall time only. + #[must_use] + pub fn wall_only(wall_time_ms: u64) -> Self { + Self::new(wall_time_ms, 0, 0) + } + + /// Sum two timings field-by-field. Used to aggregate visits of one node + /// and to accumulate run-level rollups. + #[must_use] + pub fn saturating_add(&self, other: &Self) -> Self { + Self::new( + self.wall_time_ms.saturating_add(other.wall_time_ms), + self.inference_time_ms + .saturating_add(other.inference_time_ms), + self.tool_time_ms.saturating_add(other.tool_time_ms), + ) + } +} + +/// Timing rollup for an entire run. +/// +/// `wall_time_ms` is the run's clock duration from start to terminal event. +/// The other three fields sum work across stage visits, so `active_time_ms` +/// can exceed `wall_time_ms` when parallel branches run concurrently. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct RunTiming { + /// Wall-clock time from run start to terminal event. + pub wall_time_ms: u64, + /// Sum of inference time across every stage visit. + #[serde(default)] + pub inference_time_ms: u64, + /// Sum of tool time across every stage visit. + #[serde(default)] + pub tool_time_ms: u64, + /// `inference_time_ms + tool_time_ms`. + pub active_time_ms: u64, +} + +impl RunTiming { + /// Construct a [`RunTiming`] with `active_time_ms` derived from the + /// breakdown. + #[must_use] + pub fn new(wall_time_ms: u64, inference_time_ms: u64, tool_time_ms: u64) -> Self { + let active_time_ms = inference_time_ms.saturating_add(tool_time_ms); + Self { + wall_time_ms, + inference_time_ms, + tool_time_ms, + active_time_ms, + } + } + + /// Run-level rollups for runs with no inference/tool work yet recorded. + #[must_use] + pub fn wall_only(wall_time_ms: u64) -> Self { + Self::new(wall_time_ms, 0, 0) + } + + /// Sum two run timings field-by-field. Used to accumulate aggregate + /// billing totals across completed runs. + #[must_use] + pub fn saturating_add(&self, other: &Self) -> Self { + Self::new( + self.wall_time_ms.saturating_add(other.wall_time_ms), + self.inference_time_ms + .saturating_add(other.inference_time_ms), + self.tool_time_ms.saturating_add(other.tool_time_ms), + ) + } + + /// Return a copy with `wall_time_ms` replaced. Useful at finalize time + /// when the active breakdown is summed across stage visits but the run + /// wall time is the executor's clock duration. + #[must_use] + pub fn with_wall_time(self, wall_time_ms: u64) -> Self { + Self { + wall_time_ms, + ..self + } + } +} + +impl From for RunTiming { + fn from(stage: StageTiming) -> Self { + Self { + wall_time_ms: stage.wall_time_ms, + inference_time_ms: stage.inference_time_ms, + tool_time_ms: stage.tool_time_ms, + active_time_ms: stage.active_time_ms, + } + } +} + +#[cfg(test)] +mod tests { + use super::{RunTiming, StageTiming}; + + #[test] + fn stage_timing_new_derives_active_as_sum_of_inference_and_tool() { + let timing = StageTiming::new(1000, 200, 300); + assert_eq!(timing.wall_time_ms, 1000); + assert_eq!(timing.inference_time_ms, 200); + assert_eq!(timing.tool_time_ms, 300); + assert_eq!(timing.active_time_ms, 500); + } + + #[test] + fn stage_timing_wall_only_zeroes_breakdown_and_active() { + let timing = StageTiming::wall_only(750); + assert_eq!(timing.wall_time_ms, 750); + assert_eq!(timing.inference_time_ms, 0); + assert_eq!(timing.tool_time_ms, 0); + assert_eq!(timing.active_time_ms, 0); + } + + #[test] + fn stage_timing_saturating_add_sums_all_breakdown_fields() { + let a = StageTiming::new(100, 30, 70); + let b = StageTiming::new(200, 50, 25); + let sum = a.saturating_add(&b); + assert_eq!(sum.wall_time_ms, 300); + assert_eq!(sum.inference_time_ms, 80); + assert_eq!(sum.tool_time_ms, 95); + assert_eq!(sum.active_time_ms, 175); + } + + #[test] + fn run_timing_wall_only_zeroes_breakdown_and_active() { + let timing = RunTiming::wall_only(1500); + assert_eq!(timing.wall_time_ms, 1500); + assert_eq!(timing.inference_time_ms, 0); + assert_eq!(timing.tool_time_ms, 0); + assert_eq!(timing.active_time_ms, 0); + } + + #[test] + fn run_timing_saturating_add_sums_all_breakdown_fields() { + let a = RunTiming::new(1000, 100, 50); + let b = RunTiming::new(500, 200, 150); + let sum = a.saturating_add(&b); + assert_eq!(sum.wall_time_ms, 1500); + assert_eq!(sum.inference_time_ms, 300); + assert_eq!(sum.tool_time_ms, 200); + assert_eq!(sum.active_time_ms, 500); + } + + #[test] + fn run_timing_from_stage_timing_copies_all_fields() { + let stage = StageTiming::new(900, 200, 300); + let run: RunTiming = stage.into(); + assert_eq!(run.wall_time_ms, 900); + assert_eq!(run.inference_time_ms, 200); + assert_eq!(run.tool_time_ms, 300); + assert_eq!(run.active_time_ms, 500); + } + + #[test] + fn run_timing_with_wall_time_replaces_wall_and_preserves_breakdown() { + let original = RunTiming::new(1000, 200, 300); + let updated = original.with_wall_time(5000); + assert_eq!(updated.wall_time_ms, 5000); + assert_eq!(updated.inference_time_ms, 200); + assert_eq!(updated.tool_time_ms, 300); + assert_eq!(updated.active_time_ms, 500); + } + + #[test] + fn stage_timing_round_trips_json_with_serialized_active_time() { + let original = StageTiming::new(900, 250, 350); + let json = serde_json::to_value(original).unwrap(); + assert_eq!(json["wall_time_ms"], 900); + assert_eq!(json["inference_time_ms"], 250); + assert_eq!(json["tool_time_ms"], 350); + assert_eq!(json["active_time_ms"], 600); + let parsed: StageTiming = serde_json::from_value(json).unwrap(); + assert_eq!(parsed, original); + } + + #[test] + fn run_timing_round_trips_json_with_serialized_active_time() { + let original = RunTiming::new(2000, 600, 400); + let json = serde_json::to_value(original).unwrap(); + assert_eq!(json["wall_time_ms"], 2000); + assert_eq!(json["inference_time_ms"], 600); + assert_eq!(json["tool_time_ms"], 400); + assert_eq!(json["active_time_ms"], 1000); + let parsed: RunTiming = serde_json::from_value(json).unwrap(); + assert_eq!(parsed, original); + } + + #[test] + fn stage_timing_breakdown_fields_default_when_missing_from_json() { + let json = serde_json::json!({ + "wall_time_ms": 500, + "active_time_ms": 0 + }); + let parsed: StageTiming = serde_json::from_value(json).unwrap(); + assert_eq!(parsed.wall_time_ms, 500); + assert_eq!(parsed.inference_time_ms, 0); + assert_eq!(parsed.tool_time_ms, 0); + assert_eq!(parsed.active_time_ms, 0); + } +} diff --git a/lib/crates/fabro-types/tests/run_failure_serde.rs b/lib/crates/fabro-types/tests/run_failure_serde.rs index 04b3fe00c..ca96879af 100644 --- a/lib/crates/fabro-types/tests/run_failure_serde.rs +++ b/lib/crates/fabro-types/tests/run_failure_serde.rs @@ -1,7 +1,7 @@ use fabro_types::run_event::run::RunFailedProps; use fabro_types::{ Conclusion, EventBody, ExecOutputTail, FailureCategory, FailureDetail, FailureReason, - FailureSignature, RunDiff, RunFailure, StageOutcome, SystemActorKind, + FailureSignature, RunDiff, RunFailure, RunTiming, StageOutcome, SystemActorKind, }; use serde_json::json; @@ -32,7 +32,7 @@ fn run_failed_serializes_nested_failure_contract() { detail }, }, - duration_ms: 42, + timing: RunTiming::wall_only(42), final_git_commit_sha: Some("abc123".to_string()), final_patch: Some("diff --git a/file b/file".to_string()), diff_summary: None, @@ -63,7 +63,12 @@ fn run_failed_serializes_nested_failure_contract() { } } }, - "duration_ms": 42, + "timing": { + "wall_time_ms": 42, + "inference_time_ms": 0, + "tool_time_ms": 0, + "active_time_ms": 0 + }, "final_git_commit_sha": "abc123", "final_patch": "diff --git a/file b/file" }) @@ -81,7 +86,7 @@ fn run_failed_omits_empty_failure_optional_fields() { reason: FailureReason::WorkflowError, detail: FailureDetail::new("boom", FailureCategory::Deterministic), }, - duration_ms: 1, + timing: RunTiming::wall_only(1), final_git_commit_sha: None, final_patch: None, diff_summary: None, @@ -100,7 +105,12 @@ fn run_failed_omits_empty_failure_optional_fields() { "category": "deterministic" } }, - "duration_ms": 1 + "timing": { + "wall_time_ms": 1, + "inference_time_ms": 0, + "tool_time_ms": 0, + "active_time_ms": 0 + } }) ); } @@ -114,7 +124,7 @@ fn conclusion_serializes_rich_failure() { status: StageOutcome::Failed { retry_requested: false, }, - duration_ms: 42, + timing: RunTiming::wall_only(42), failure: Some(RunFailure { reason: FailureReason::WorkflowError, detail: { diff --git a/lib/crates/fabro-workflow/src/billing_rollup.rs b/lib/crates/fabro-workflow/src/billing_rollup.rs index a796258a3..0e909ce08 100644 --- a/lib/crates/fabro-workflow/src/billing_rollup.rs +++ b/lib/crates/fabro-workflow/src/billing_rollup.rs @@ -2,7 +2,9 @@ use std::borrow::Cow; use std::collections::HashMap; use fabro_model::Catalog; -use fabro_types::{BilledTokenCounts, ModelRef, RunProjection, StageProjection}; +use fabro_types::{ + BilledTokenCounts, ModelRef, RunProjection, RunTiming, StageProjection, StageTiming, +}; fn stage_usage_with_cost<'a>( catalog: Option<&Catalog>, @@ -28,10 +30,13 @@ fn stage_usage_with_cost<'a>( #[derive(Debug, Clone, PartialEq)] pub struct ProjectionBillingStage { - pub node_id: String, - pub billing: BilledTokenCounts, - pub duration_ms: u64, - pub model: Option, + pub node_id: String, + pub billing: BilledTokenCounts, + /// Per-node timing summed across every visit of that node within this + /// projection. `wall_time_ms`, `inference_time_ms`, `tool_time_ms`, and + /// `active_time_ms` are all summed in lockstep. + pub timing: StageTiming, + pub model: Option, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -46,7 +51,9 @@ pub struct ProjectionBillingRollup { pub stages: Vec, pub totals: BilledTokenCounts, pub by_model: Vec, - pub runtime_ms: u64, + /// Run-level timing summed across every stage visit. `wall_time_ms` is + /// the sum of stage visit wall times (not the run clock duration). + pub timing: RunTiming, pub billed_visit_count: usize, } @@ -66,7 +73,7 @@ pub fn billing_rollup_from_projection( let mut stages = Vec::::new(); let mut by_model = HashMap::::new(); let mut totals = BilledTokenCounts::default(); - let mut runtime_ms = 0_u64; + let mut run_timing = RunTiming::default(); let mut billed_visit_count = 0_usize; for (stage_id, stage) in projection.iter_stages() { @@ -75,7 +82,7 @@ pub fn billing_rollup_from_projection( } let usage = stage_usage_with_cost(catalog, stage); let usage = usage.as_ref(); - if stage.completion.is_none() && stage.duration_ms.is_none() && usage.is_zero() { + if stage.completion.is_none() && stage.timing.is_none() && usage.is_zero() { continue; } @@ -83,18 +90,18 @@ pub fn billing_rollup_from_projection( let index = *stage_indices.entry(node_id.to_string()).or_insert_with(|| { let index = stages.len(); stages.push(ProjectionBillingStage { - node_id: node_id.to_string(), - billing: BilledTokenCounts::default(), - duration_ms: 0, - model: None, + node_id: node_id.to_string(), + billing: BilledTokenCounts::default(), + timing: StageTiming::default(), + model: None, }); index }); let row = &mut stages[index]; - if let Some(duration_ms) = stage.duration_ms { - row.duration_ms = row.duration_ms.saturating_add(duration_ms); - runtime_ms = runtime_ms.saturating_add(duration_ms); + if let Some(timing) = stage.timing { + row.timing = row.timing.saturating_add(&timing); + run_timing = run_timing.saturating_add(&RunTiming::from(timing)); } if !usage.is_zero() { @@ -137,7 +144,7 @@ pub fn billing_rollup_from_projection( stages, totals, by_model, - runtime_ms, + timing: run_timing, billed_visit_count, } } @@ -198,7 +205,7 @@ mod tests { let failed_usage = test_usage("gpt-old", 100, 10); let success_usage = test_usage("gpt-new", 200, 20); let first = projection.stage_entry("verify", 1, first_event_seq(1)); - first.duration_ms = Some(1200); + first.timing = Some(fabro_types::StageTiming::wall_only(1200)); first.usage = BilledTokenCounts::from_billed_usage(std::slice::from_ref(&failed_usage)); first.model = Some(failed_usage.model().clone()); first.completion = Some(StageCompletion { @@ -210,7 +217,7 @@ mod tests { timestamp: chrono::Utc::now(), }); let second = projection.stage_entry("verify", 2, first_event_seq(2)); - second.duration_ms = Some(800); + second.timing = Some(fabro_types::StageTiming::wall_only(800)); second.usage = BilledTokenCounts::from_billed_usage(std::slice::from_ref(&success_usage)); second.model = Some(success_usage.model().clone()); second.completion = Some(StageCompletion { @@ -231,12 +238,12 @@ mod tests { .map(|model| model.model_id.as_str()), Some("gpt-new") ); - assert_eq!(rollup.stages[0].duration_ms, 2000); + assert_eq!(rollup.stages[0].timing.wall_time_ms, 2000); assert_eq!(rollup.stages[0].billing.input_tokens, 300); assert_eq!(rollup.stages[0].billing.output_tokens, 30); assert_eq!(rollup.stages[0].billing.total_usd_micros, Some(330)); - assert_eq!(rollup.runtime_ms, 2000); + assert_eq!(rollup.timing.wall_time_ms, 2000); assert_eq!(rollup.totals.input_tokens, 300); assert_eq!(rollup.totals.output_tokens, 30); assert_eq!(rollup.totals.total_usd_micros, Some(330)); @@ -255,7 +262,7 @@ mod tests { fn rollup_includes_completed_non_llm_stage_rows_with_zero_billing() { let mut projection = test_projection(); let stage = projection.stage_entry("build", 1, first_event_seq(1)); - stage.duration_ms = Some(25); + stage.timing = Some(fabro_types::StageTiming::wall_only(25)); stage.completion = Some(StageCompletion { outcome: StageOutcome::Succeeded, notes: None, @@ -267,10 +274,10 @@ mod tests { assert_eq!(rollup.stages.len(), 1); assert_eq!(rollup.stages[0].node_id, "build"); - assert_eq!(rollup.stages[0].duration_ms, 25); + assert_eq!(rollup.stages[0].timing.wall_time_ms, 25); assert!(rollup.stages[0].model.is_none()); assert_eq!(rollup.stages[0].billing.input_tokens, 0); - assert_eq!(rollup.runtime_ms, 25); + assert_eq!(rollup.timing.wall_time_ms, 25); assert!(rollup.by_model.is_empty()); assert!(rollup.billing_if_present().is_none()); } @@ -280,7 +287,7 @@ mod tests { let mut projection = test_projection(); projection.spec = run_spec_with_boundary_nodes(); let start = projection.stage_entry("start", 1, first_event_seq(1)); - start.duration_ms = Some(25); + start.timing = Some(fabro_types::StageTiming::wall_only(25)); start.completion = Some(StageCompletion { outcome: StageOutcome::Succeeded, notes: None, @@ -288,7 +295,7 @@ mod tests { timestamp: chrono::Utc::now(), }); let exit = projection.stage_entry("exit", 1, first_event_seq(2)); - exit.duration_ms = Some(7); + exit.timing = Some(fabro_types::StageTiming::wall_only(7)); exit.completion = Some(StageCompletion { outcome: StageOutcome::Succeeded, notes: None, @@ -299,7 +306,7 @@ mod tests { let rollup = billing_rollup_from_projection(&projection, None); assert_eq!(rollup.stages.len(), 0); - assert_eq!(rollup.runtime_ms, 0); + assert_eq!(rollup.timing.wall_time_ms, 0); } #[test] diff --git a/lib/crates/fabro-workflow/src/error.rs b/lib/crates/fabro-workflow/src/error.rs index 89a2a8952..6fcd565e3 100644 --- a/lib/crates/fabro-workflow/src/error.rs +++ b/lib/crates/fabro-workflow/src/error.rs @@ -2037,14 +2037,14 @@ mod tests { // 3. Outcome → StageFailed event let failure = outcome.failure.clone().unwrap(); let event = Event::StageFailed { - node_id: "code".into(), - name: "code".into(), - index: 0, - failure: failure.clone(), - will_retry: false, - duration_ms: 0, - billing: None, - actor: None, + node_id: "code".into(), + name: "code".into(), + index: 0, + failure: failure.clone(), + will_retry: false, + timing: fabro_types::StageTiming::wall_only(0), + billing: None, + actor: None, }; // 4. Verify classification survived all the way through diff --git a/lib/crates/fabro-workflow/src/event/convert.rs b/lib/crates/fabro-workflow/src/event/convert.rs index a93cb235c..4e823e4b8 100644 --- a/lib/crates/fabro-workflow/src/event/convert.rs +++ b/lib/crates/fabro-workflow/src/event/convert.rs @@ -181,7 +181,7 @@ fn event_body_from_event(event: &Event) -> EventBody { previous_parent_id: *previous_parent_id, }), Event::WorkflowRunCompleted { - duration_ms, + timing, artifact_count, status, reason, @@ -191,7 +191,7 @@ fn event_body_from_event(event: &Event) -> EventBody { diff_summary, billing, } => EventBody::RunCompleted(fabro_types::RunCompletedProps { - duration_ms: *duration_ms, + timing: *timing, artifact_count: *artifact_count, status: status.clone(), reason: *reason, @@ -203,14 +203,14 @@ fn event_body_from_event(event: &Event) -> EventBody { }), Event::WorkflowRunFailed { failure, - duration_ms, + timing, final_git_commit_sha, final_patch, diff_summary, billing, } => EventBody::RunFailed(fabro_types::RunFailedProps { failure: failure.clone(), - duration_ms: *duration_ms, + timing: *timing, final_git_commit_sha: final_git_commit_sha.clone(), final_patch: final_patch.clone(), diff_summary: *diff_summary, @@ -285,7 +285,7 @@ fn event_body_from_event(event: &Event) -> EventBody { }), Event::StageCompleted { index, - duration_ms, + timing, status, preferred_label, suggested_next_ids, @@ -305,7 +305,7 @@ fn event_body_from_event(event: &Event) -> EventBody { .. } => EventBody::StageCompleted(fabro_types::StageCompletedProps { index: *index, - duration_ms: *duration_ms, + timing: *timing, status: stage_status_from_string(status), preferred_label: preferred_label.clone(), suggested_next_ids: suggested_next_ids.clone(), @@ -327,15 +327,15 @@ fn event_body_from_event(event: &Event) -> EventBody { index, failure, will_retry, - duration_ms, + timing, billing, .. } => EventBody::StageFailed(fabro_types::StageFailedProps { - index: *index, - failure: Some(failure.clone()), - will_retry: *will_retry, - duration_ms: *duration_ms, - billing: billing.clone(), + index: *index, + failure: Some(failure.clone()), + will_retry: *will_retry, + timing: *timing, + billing: billing.clone(), }), Event::StageRetrying { index, @@ -1366,7 +1366,7 @@ mod tests { node_id: "plan".to_string(), name: "Plan".to_string(), index: 0, - duration_ms: 5000, + timing: ::fabro_types::StageTiming::wall_only(5000), status: "succeeded".to_string(), preferred_label: None, suggested_next_ids: Vec::new(), @@ -1399,7 +1399,8 @@ mod tests { assert_eq!(stored.node_label.as_deref(), Some("Plan")); assert_eq!(stored.stage_id, Some(StageId::new("plan", 1))); let properties = stored.properties().unwrap(); - assert_eq!(properties["duration_ms"], 5000); + assert_eq!(properties["timing"]["wall_time_ms"], 5000); + assert_eq!(properties["timing"]["active_time_ms"], 0); assert_eq!(properties["status"], "succeeded"); assert!(stored.session_id.is_none()); } @@ -1410,7 +1411,7 @@ mod tests { node_id: "plan".to_string(), name: "Plan".to_string(), index: 0, - duration_ms: 5000, + timing: ::fabro_types::StageTiming::wall_only(5000), status: "succeeded".to_string(), preferred_label: None, suggested_next_ids: Vec::new(), @@ -1439,17 +1440,17 @@ mod tests { fn run_event_stage_failure_keeps_failure_detail() { let usage = test_usage("gpt-5.2", 321, 54); let stored = to_run_event(&fixtures::RUN_3, &Event::StageFailed { - node_id: "code".to_string(), - name: "Code".to_string(), - index: 1, - failure: FailureDetail::new( + node_id: "code".to_string(), + name: "Code".to_string(), + index: 1, + failure: FailureDetail::new( "lint failed", crate::outcome::FailureCategory::Deterministic, ), - will_retry: true, - duration_ms: 5000, - billing: Some(usage.clone()), - actor: None, + will_retry: true, + timing: ::fabro_types::StageTiming::wall_only(5000), + billing: Some(usage.clone()), + actor: None, }); assert_eq!(stored.event_name(), "stage.failed"); @@ -1552,7 +1553,7 @@ mod tests { fn run_event_workflow_failure_uses_display_error() { let event = Event::workflow_run_failed_from_error( &Error::handler("boom"), - 900, + ::fabro_types::RunTiming::wall_only(900), FailureReason::WorkflowError, Some("abc123".to_string()), None, @@ -1564,7 +1565,7 @@ mod tests { assert_eq!(stored.event_name(), "run.failed"); let properties = stored.properties().unwrap(); assert_eq!(properties["failure"]["detail"]["message"], "boom"); - assert_eq!(properties["duration_ms"], 900); + assert_eq!(properties["timing"]["wall_time_ms"], 900); } #[test] @@ -1572,7 +1573,7 @@ mod tests { let source = EventTestCause; let event = Event::workflow_run_failed_from_error( &Error::engine_with_source("Failed to initialize sandbox", source), - 900, + ::fabro_types::RunTiming::wall_only(900), FailureReason::WorkflowError, None, None, @@ -1597,7 +1598,7 @@ mod tests { let source = EventTestCause; let event = Event::workflow_run_failed_from_error( &Error::engine_with_source("Failed to initialize sandbox", source), - 900, + ::fabro_types::RunTiming::wall_only(900), FailureReason::SandboxInitFailed, Some("abc123".to_string()), None, @@ -1621,7 +1622,7 @@ mod tests { properties["failure"]["detail"]["category"], "transient_infra" ); - assert_eq!(properties["duration_ms"], 900); + assert_eq!(properties["timing"]["wall_time_ms"], 900); assert_eq!(properties["final_git_commit_sha"], "abc123"); assert!(properties.get("error").is_none()); assert!(properties.get("causes").is_none()); diff --git a/lib/crates/fabro-workflow/src/event/events.rs b/lib/crates/fabro-workflow/src/event/events.rs index e4648e3f3..adcbe216b 100644 --- a/lib/crates/fabro-workflow/src/event/events.rs +++ b/lib/crates/fabro-workflow/src/event/events.rs @@ -4,8 +4,8 @@ use ::fabro_types::{ BilledTokenCounts, BlockedReason, CommandTermination, DiffSummary, FailureReason, ForkSourceRef, GitContext, PairId, PairMessageId, PairSystemMessageKind, PairTarget, ParallelBranchId, Principal, PullRequestLink, RunBlobId, RunFailure, RunId, RunNoticeLevel, - RunPairEndedReason, RunPairFailedReason, RunProvenance, SandboxProvider, StageId, - SuccessReason, run_event as fabro_types, + RunPairEndedReason, RunPairFailedReason, RunProvenance, RunTiming, SandboxProvider, StageId, + StageTiming, SuccessReason, run_event as fabro_types, }; use fabro_agent::{AgentEvent, SandboxEvent}; use serde::{Deserialize, Serialize}; @@ -150,7 +150,7 @@ pub enum Event { actor: Option, }, WorkflowRunCompleted { - duration_ms: u64, + timing: RunTiming, artifact_count: usize, #[serde(default)] status: String, @@ -168,7 +168,7 @@ pub enum Event { }, WorkflowRunFailed { failure: RunFailure, - duration_ms: u64, + timing: RunTiming, #[serde(default, skip_serializing_if = "Option::is_none")] final_git_commit_sha: Option, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -226,7 +226,7 @@ pub enum Event { node_id: String, name: String, index: usize, - duration_ms: u64, + timing: StageTiming, status: String, preferred_label: Option, suggested_next_ids: Vec, @@ -253,15 +253,15 @@ pub enum Event { max_attempts: usize, }, StageFailed { - node_id: String, - name: String, - index: usize, - failure: FailureDetail, - will_retry: bool, - duration_ms: u64, - billing: Option, + node_id: String, + name: String, + index: usize, + failure: FailureDetail, + will_retry: bool, + timing: StageTiming, + billing: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - actor: Option, + actor: Option, }, StageRetrying { node_id: String, @@ -724,7 +724,7 @@ impl Event { #[must_use] pub fn workflow_run_failed_from_error( error: &Error, - duration_ms: u64, + timing: RunTiming, reason: FailureReason, final_git_commit_sha: Option, final_patch: Option, @@ -733,7 +733,7 @@ impl Event { ) -> Self { Self::WorkflowRunFailed { failure: run_failure_from_error(error, reason), - duration_ms, + timing, final_git_commit_sha, final_patch, diff_summary, @@ -868,20 +868,23 @@ impl Event { info!(%previous_parent_id, ?actor, "Run parent unlinked"); } Self::WorkflowRunCompleted { - duration_ms, + timing, artifact_count, status, .. } => { info!( - duration_ms, - artifact_count, status, "Workflow run completed" + wall_time_ms = timing.wall_time_ms, + active_time_ms = timing.active_time_ms, + inference_time_ms = timing.inference_time_ms, + tool_time_ms = timing.tool_time_ms, + artifact_count, + status, + "Workflow run completed" ); } Self::WorkflowRunFailed { - failure, - duration_ms, - .. + failure, timing, .. } => { let detail = &failure.detail; let tail = @@ -898,7 +901,8 @@ impl Event { exec_stderr_tail_bytes = tail.stderr_bytes, exec_stdout_truncated = tail.stdout_truncated, exec_stderr_truncated = tail.stderr_truncated, - duration_ms, + wall_time_ms = timing.wall_time_ms, + active_time_ms = timing.active_time_ms, "Workflow run failed" ); } @@ -1009,7 +1013,7 @@ impl Event { node_id, name, index, - duration_ms, + timing, status, attempt, max_attempts, @@ -1019,7 +1023,10 @@ impl Event { node_id, stage = name.as_str(), index, - duration_ms, + wall_time_ms = timing.wall_time_ms, + active_time_ms = timing.active_time_ms, + inference_time_ms = timing.inference_time_ms, + tool_time_ms = timing.tool_time_ms, status, attempt, max_attempts, diff --git a/lib/crates/fabro-workflow/src/git.rs b/lib/crates/fabro-workflow/src/git.rs index 80220c4fa..98c916d9b 100644 --- a/lib/crates/fabro-workflow/src/git.rs +++ b/lib/crates/fabro-workflow/src/git.rs @@ -500,7 +500,7 @@ mod tests { node_id: "work".into(), name: "Work".into(), index: 2, - duration_ms: 100, + timing: fabro_types::StageTiming::wall_only(100), status: "succeeded".into(), preferred_label: None, suggested_next_ids: Vec::new(), diff --git a/lib/crates/fabro-workflow/src/handler/llm/api.rs b/lib/crates/fabro-workflow/src/handler/llm/api.rs index 01bc9fe8a..42313eda2 100644 --- a/lib/crates/fabro-workflow/src/handler/llm/api.rs +++ b/lib/crates/fabro-workflow/src/handler/llm/api.rs @@ -1635,9 +1635,8 @@ mod tests { started_at: None, last_event_at: None, completed_at: None, - duration_ms: None, - elapsed_secs: None, }, + timing: None, billing: None, diff: None, pull_request: None, diff --git a/lib/crates/fabro-workflow/src/lib.rs b/lib/crates/fabro-workflow/src/lib.rs index 5298ec446..194a0ca2c 100644 --- a/lib/crates/fabro-workflow/src/lib.rs +++ b/lib/crates/fabro-workflow/src/lib.rs @@ -29,57 +29,67 @@ pub(crate) fn millis_u64(d: std::time::Duration) -> u64 { u64::try_from(d.as_millis()).unwrap_or(u64::MAX) } -/// Extract the `duration_ms` from a `stage.completed` / `stage.failed` +/// Extract the timing breakdown from a `stage.completed` / `stage.failed` /// event body, or `None` for any other variant. -fn stage_completion_duration_ms(body: &EventBody) -> Option { +fn stage_completion_timing(body: &EventBody) -> Option { match body { - EventBody::StageCompleted(props) => Some(props.duration_ms), - EventBody::StageFailed(props) => Some(props.duration_ms), + EventBody::StageCompleted(props) => Some(props.timing), + EventBody::StageFailed(props) => Some(props.timing), _ => None, } } -/// Extract per-stage (node_id, visit) durations from `stage.completed` / +/// Extract per-stage (node_id, visit) timing from `stage.completed` / /// `stage.failed` events. Keys on the full [`StageId`] so multi-visit stages -/// (e.g. a looped `verify` node) keep distinct durations. +/// (e.g. a looped `verify` node) keep distinct timings. /// -/// This is the canonical primitive; [`total_stage_duration_by_node`] and -/// [`latest_stage_duration_by_node`] are explicit rollups built on top of it. -pub fn extract_stage_durations_by_stage_id(events: &[EventEnvelope]) -> HashMap { - let mut durations = HashMap::new(); +/// This is the canonical primitive; [`total_stage_timing_by_node`] and +/// [`latest_stage_timing_by_node`] are explicit rollups built on top of it. +pub fn extract_stage_timings_by_stage_id( + events: &[EventEnvelope], +) -> HashMap { + let mut timings = HashMap::new(); for envelope in events { - let Some(duration_ms) = stage_completion_duration_ms(&envelope.event.body) else { + let Some(timing) = stage_completion_timing(&envelope.event.body) else { continue; }; let Some(stage_id) = envelope.event.stage_id.as_ref() else { continue; }; - durations.insert(stage_id.clone(), duration_ms); + timings.insert(stage_id.clone(), timing); } - durations + timings } -/// Total duration spent in each node, summed across every visit. Use for -/// billing/usage where a retried node should count its full time. -pub fn total_stage_duration_by_node(events: &[EventEnvelope]) -> HashMap { - let mut totals: HashMap = HashMap::new(); - for (stage_id, duration_ms) in extract_stage_durations_by_stage_id(events) { - *totals.entry(stage_id.node_id().to_string()).or_default() += duration_ms; +/// Sum of timing in each node across every visit. Use for billing/usage +/// where a retried node should count its full time. `wall_time_ms`, +/// `inference_time_ms`, `tool_time_ms`, and `active_time_ms` are all summed +/// per node. +pub fn total_stage_timing_by_node( + events: &[EventEnvelope], +) -> HashMap { + let mut totals: HashMap = HashMap::new(); + for (stage_id, timing) in extract_stage_timings_by_stage_id(events) { + let entry = totals.entry(stage_id.node_id().to_string()).or_default(); + *entry = entry.saturating_add(&timing); } totals } -/// Duration of each node's most recent visit (the highest visit number). Use +/// Timing of each node's most recent visit (the highest visit number). Use /// for run summaries where the table shows one row per node and "the last /// attempt" is the right representative. -pub fn latest_stage_duration_by_node(events: &[EventEnvelope]) -> HashMap { - let mut entries: Vec<(StageId, u64)> = extract_stage_durations_by_stage_id(events) - .into_iter() - .collect(); +pub fn latest_stage_timing_by_node( + events: &[EventEnvelope], +) -> HashMap { + let mut entries: Vec<(StageId, fabro_types::StageTiming)> = + extract_stage_timings_by_stage_id(events) + .into_iter() + .collect(); entries.sort_by_key(|(stage_id, _)| stage_id.visit()); let mut latest = HashMap::new(); - for (stage_id, duration_ms) in entries { - latest.insert(stage_id.node_id().to_string(), duration_ms); + for (stage_id, timing) in entries { + latest.insert(stage_id.node_id().to_string(), timing); } latest } @@ -89,14 +99,13 @@ mod duration_tests { use chrono::{TimeZone, Utc}; use fabro_store::EventEnvelope; use fabro_types::run_event::{StageCompletedProps, StageFailedProps}; - use fabro_types::{EventBody, RunEvent, StageId, StageOutcome, fixtures}; + use fabro_types::{EventBody, RunEvent, StageId, StageOutcome, StageTiming, fixtures}; use super::{ - extract_stage_durations_by_stage_id, latest_stage_duration_by_node, - total_stage_duration_by_node, + extract_stage_timings_by_stage_id, latest_stage_timing_by_node, total_stage_timing_by_node, }; - fn completed_event(seq: u32, node: &str, visit: u32, duration_ms: u64) -> EventEnvelope { + fn completed_event(seq: u32, node: &str, visit: u32, wall_time_ms: u64) -> EventEnvelope { let event = RunEvent { id: format!("evt_{seq}"), ts: Utc.with_ymd_and_hms(2026, 1, 1, 0, 0, 0).unwrap(), @@ -112,7 +121,7 @@ mod duration_tests { actor: None, body: EventBody::StageCompleted(StageCompletedProps { index: 0, - duration_ms, + timing: StageTiming::wall_only(wall_time_ms), status: StageOutcome::Succeeded, preferred_label: None, suggested_next_ids: vec![], @@ -134,7 +143,7 @@ mod duration_tests { EventEnvelope { seq, event } } - fn failed_event(seq: u32, node: &str, visit: u32, duration_ms: u64) -> EventEnvelope { + fn failed_event(seq: u32, node: &str, visit: u32, wall_time_ms: u64) -> EventEnvelope { let event = RunEvent { id: format!("evt_{seq}"), ts: Utc.with_ymd_and_hms(2026, 1, 1, 0, 0, 0).unwrap(), @@ -149,62 +158,126 @@ mod duration_tests { tool_call_id: None, actor: None, body: EventBody::StageFailed(StageFailedProps { - index: 0, - failure: None, + index: 0, + failure: None, will_retry: true, - duration_ms, - billing: None, + timing: StageTiming::wall_only(wall_time_ms), + billing: None, }), }; EventEnvelope { seq, event } } #[test] - fn extract_keys_durations_by_full_stage_id() { + fn extract_keys_timings_by_full_stage_id() { let events = vec![ completed_event(1, "verify", 1, 100), completed_event(2, "verify", 2, 200), ]; - let durations = extract_stage_durations_by_stage_id(&events); + let timings = extract_stage_timings_by_stage_id(&events); assert_eq!( - durations.get(&StageId::new("verify", 1)).copied(), + timings + .get(&StageId::new("verify", 1)) + .map(|t| t.wall_time_ms), Some(100) ); assert_eq!( - durations.get(&StageId::new("verify", 2)).copied(), + timings + .get(&StageId::new("verify", 2)) + .map(|t| t.wall_time_ms), Some(200) ); } #[test] - fn total_sums_across_visits_per_node() { + fn total_sums_wall_time_across_visits_per_node() { let events = vec![ completed_event(1, "verify", 1, 100), completed_event(2, "verify", 2, 200), completed_event(3, "build", 1, 50), ]; - let totals = total_stage_duration_by_node(&events); - assert_eq!(totals.get("verify").copied(), Some(300)); - assert_eq!(totals.get("build").copied(), Some(50)); + let totals = total_stage_timing_by_node(&events); + assert_eq!(totals.get("verify").map(|t| t.wall_time_ms), Some(300)); + assert_eq!(totals.get("build").map(|t| t.wall_time_ms), Some(50)); } #[test] fn latest_picks_highest_visit_regardless_of_input_order() { // Visit 2 appears in the events vector before visit 1; the result - // must still reflect visit 2's duration (the latest visit). + // must still reflect visit 2's timing (the latest visit). let events = vec![ completed_event(1, "verify", 2, 999), completed_event(2, "verify", 1, 100), ]; - let latest = latest_stage_duration_by_node(&events); - assert_eq!(latest.get("verify").copied(), Some(999)); + let latest = latest_stage_timing_by_node(&events); + assert_eq!(latest.get("verify").map(|t| t.wall_time_ms), Some(999)); } #[test] - fn stage_failed_durations_are_included() { + fn stage_failed_timings_are_included() { let events = vec![failed_event(1, "verify", 1, 75)]; - let durations = extract_stage_durations_by_stage_id(&events); - assert_eq!(durations.get(&StageId::new("verify", 1)).copied(), Some(75)); + let timings = extract_stage_timings_by_stage_id(&events); + assert_eq!( + timings + .get(&StageId::new("verify", 1)) + .map(|t| t.wall_time_ms), + Some(75) + ); + } + + #[test] + fn total_sums_active_breakdown_across_visits() { + // Same node visited twice with different inference/tool breakdowns: + // the rollup must add inference, tool, and active fields, not just + // wall time. This guards against accidentally summing wall only. + fn timed_completed(seq: u32, visit: u32, timing: StageTiming) -> EventEnvelope { + let event = RunEvent { + id: format!("evt_{seq}"), + ts: Utc.with_ymd_and_hms(2026, 1, 1, 0, 0, 0).unwrap(), + run_id: fixtures::RUN_1, + node_id: Some("agent".to_string()), + node_label: None, + stage_id: Some(StageId::new("agent", visit)), + parallel_group_id: None, + parallel_branch_id: None, + session_id: None, + parent_session_id: None, + tool_call_id: None, + actor: None, + body: EventBody::StageCompleted(StageCompletedProps { + index: 0, + timing, + status: StageOutcome::Succeeded, + preferred_label: None, + suggested_next_ids: vec![], + billing: None, + failure: None, + notes: None, + files_touched: vec![], + context_updates: None, + jump_to_node: None, + context_values: None, + node_visits: None, + loop_failure_signatures: None, + restart_failure_signatures: None, + response: None, + attempt: 1, + max_attempts: 1, + }), + }; + EventEnvelope { seq, event } + } + + let events = vec![ + timed_completed(1, 1, StageTiming::new(1000, 600, 300)), + timed_completed(2, 2, StageTiming::new(700, 400, 200)), + ]; + let totals = total_stage_timing_by_node(&events); + let agent = totals.get("agent").copied().unwrap(); + assert_eq!(agent.wall_time_ms, 1700); + assert_eq!(agent.inference_time_ms, 1000); + assert_eq!(agent.tool_time_ms, 500); + assert_eq!(agent.active_time_ms, 1500); } } diff --git a/lib/crates/fabro-workflow/src/lifecycle/event.rs b/lib/crates/fabro-workflow/src/lifecycle/event.rs index fd04a186f..3226abac1 100644 --- a/lib/crates/fabro-workflow/src/lifecycle/event.rs +++ b/lib/crates/fabro-workflow/src/lifecycle/event.rs @@ -10,7 +10,7 @@ use fabro_core::lifecycle::{ }; use fabro_core::outcome::NodeResult; use fabro_core::state::ExecutionState; -use fabro_types::{Principal, RunId}; +use fabro_types::{Principal, RunId, StageTiming}; use super::circuit_breaker::CircuitBreakerLifecycle; use super::git::GitCheckpointResult; @@ -73,6 +73,18 @@ fn actor_for_stage_failure(failure: &FailureDetail) -> Option { .map(|system_kind| Principal::System { system_kind }) } +/// Build a [`StageTiming`] from a [`WfNodeResult`]. Inference and tool time +/// flow from the executor's `NodeResult` fields, which are populated from +/// `outcome.timing` by [`fabro_core`]. Handlers without an active-time +/// breakdown produce a wall-only timing. +fn node_result_timing(result: &WfNodeResult) -> StageTiming { + StageTiming::new( + crate::millis_u64(result.wall_time), + crate::millis_u64(result.inference_time), + crate::millis_u64(result.tool_time), + ) +} + fn response_from_outcome(node_id: &str, outcome: &Outcome) -> Option { outcome .context_updates @@ -154,7 +166,7 @@ impl RunLifecycle for EventLifecycle { node_id: gv.id.clone(), name: gv.label().to_string(), index: stage_index, - duration_ms: 0, + timing: StageTiming::wall_only(0), status: StageOutcome::Succeeded.to_string(), preferred_label: None, suggested_next_ids: Vec::new(), @@ -211,7 +223,7 @@ impl RunLifecycle for EventLifecycle { let stage_index = state.stage_index; let scope = stage_scope_for(state, &gv.id); - let duration_ms = crate::millis_u64(ctx.result.duration); + let timing = node_result_timing(ctx.result); let failure = outcome.failure.clone().unwrap_or_else(|| { FailureDetail::new("handler failed", FailureCategory::TransientInfra) }); @@ -223,7 +235,7 @@ impl RunLifecycle for EventLifecycle { index: stage_index, failure, will_retry: true, - duration_ms, + timing, billing: outcome.usage.clone(), actor, }, @@ -259,7 +271,7 @@ impl RunLifecycle for EventLifecycle { let gv = node.inner(); let stage_index = state.stage_index; let scope = stage_scope_for(state, &gv.id); - let duration_ms = crate::millis_u64(result.duration); + let timing = node_result_timing(result); let (loop_failure_signatures, restart_failure_signatures) = snapshot_failure_signatures(&self.circuit_breaker); @@ -275,7 +287,7 @@ impl RunLifecycle for EventLifecycle { index: stage_index, failure, will_retry: false, - duration_ms, + timing, billing: outcome.usage.clone(), actor, }, @@ -287,7 +299,7 @@ impl RunLifecycle for EventLifecycle { node_id: gv.id.clone(), name: gv.label().to_string(), index: stage_index, - duration_ms, + timing, status: outcome.status.to_string(), preferred_label: outcome.preferred_label.clone(), suggested_next_ids: outcome.suggested_next_ids.clone(), diff --git a/lib/crates/fabro-workflow/src/lifecycle/git.rs b/lib/crates/fabro-workflow/src/lifecycle/git.rs index ddbd15f07..4e8fef87f 100644 --- a/lib/crates/fabro-workflow/src/lifecycle/git.rs +++ b/lib/crates/fabro-workflow/src/lifecycle/git.rs @@ -956,7 +956,14 @@ mod tests { let node = graph.get_node("build").unwrap(); let mut state = ExecutionState::new(&graph).unwrap(); state.increment_visits("build"); - let result = WfNodeResult::new(Outcome::success(), Duration::from_millis(10), 1, 1); + let result = WfNodeResult::new( + Outcome::success(), + Duration::from_millis(10), + Duration::ZERO, + Duration::ZERO, + 1, + 1, + ); lifecycle .on_checkpoint(&node, &result, Some("exit"), &state) @@ -999,7 +1006,14 @@ mod tests { let node = graph.get_node("build").unwrap(); let mut state = ExecutionState::new(&graph).unwrap(); state.increment_visits("build"); - let result = WfNodeResult::new(Outcome::success(), Duration::from_millis(10), 1, 1); + let result = WfNodeResult::new( + Outcome::success(), + Duration::from_millis(10), + Duration::ZERO, + Duration::ZERO, + 1, + 1, + ); lifecycle .on_checkpoint(&node, &result, Some("exit"), &state) @@ -1060,7 +1074,14 @@ mod tests { let node = graph.get_node("build").unwrap(); let mut state = ExecutionState::new(&graph).unwrap(); state.increment_visits("build"); - let result = WfNodeResult::new(Outcome::success(), Duration::from_millis(10), 1, 1); + let result = WfNodeResult::new( + Outcome::success(), + Duration::from_millis(10), + Duration::ZERO, + Duration::ZERO, + 1, + 1, + ); lifecycle .on_checkpoint(&node, &result, Some("exit"), &state) @@ -1127,7 +1148,14 @@ mod tests { let node = graph.get_node("build").unwrap(); let mut state = ExecutionState::new(&graph).unwrap(); state.increment_visits("build"); - let result = WfNodeResult::new(Outcome::success(), Duration::from_millis(10), 1, 1); + let result = WfNodeResult::new( + Outcome::success(), + Duration::from_millis(10), + Duration::ZERO, + Duration::ZERO, + 1, + 1, + ); lifecycle .on_checkpoint(&node, &result, Some("exit"), &state) @@ -1189,7 +1217,14 @@ mod tests { let node = graph.get_node("build").unwrap(); let mut checkpoint_state = ExecutionState::new(&graph).unwrap(); checkpoint_state.increment_visits("build"); - let result = WfNodeResult::new(Outcome::success(), Duration::from_millis(10), 1, 1); + let result = WfNodeResult::new( + Outcome::success(), + Duration::from_millis(10), + Duration::ZERO, + Duration::ZERO, + 1, + 1, + ); lifecycle .on_checkpoint(&node, &result, Some("exit"), &checkpoint_state) .await @@ -1220,7 +1255,7 @@ mod tests { let conclusion = Conclusion { timestamp: chrono::Utc::now(), status: StageOutcome::Succeeded, - duration_ms: 10, + timing: fabro_types::RunTiming::wall_only(10), failure: None, final_git_commit_sha: None, stages: Vec::new(), diff --git a/lib/crates/fabro-workflow/src/operations/archive.rs b/lib/crates/fabro-workflow/src/operations/archive.rs index a0fea188c..34ca62650 100644 --- a/lib/crates/fabro-workflow/src/operations/archive.rs +++ b/lib/crates/fabro-workflow/src/operations/archive.rs @@ -160,7 +160,7 @@ mod tests { .await .unwrap(); event::append_event(&run_store, run_id, &Event::WorkflowRunCompleted { - duration_ms: 10, + timing: fabro_types::RunTiming::wall_only(10), artifact_count: 0, status: "succeeded".to_string(), reason: SuccessReason::Completed, @@ -185,7 +185,7 @@ mod tests { .unwrap(); let failure_event = Event::workflow_run_failed_from_error( &crate::error::Error::engine("boom"), - 10, + fabro_types::RunTiming::wall_only(10), FailureReason::WorkflowError, None, None, diff --git a/lib/crates/fabro-workflow/src/operations/fork.rs b/lib/crates/fabro-workflow/src/operations/fork.rs index 6379274ed..3731e6145 100644 --- a/lib/crates/fabro-workflow/src/operations/fork.rs +++ b/lib/crates/fabro-workflow/src/operations/fork.rs @@ -392,7 +392,7 @@ mod tests { node_id: "work".to_string(), name: "Work".to_string(), index: 1, - duration_ms: 10, + timing: fabro_types::StageTiming::wall_only(10), status: "succeeded".to_string(), preferred_label: None, suggested_next_ids: Vec::new(), diff --git a/lib/crates/fabro-workflow/src/operations/start.rs b/lib/crates/fabro-workflow/src/operations/start.rs index cd59dced0..1a3e2f6cc 100644 --- a/lib/crates/fabro-workflow/src/operations/start.rs +++ b/lib/crates/fabro-workflow/src/operations/start.rs @@ -275,7 +275,7 @@ async fn persist_terminal_engine_failure( }; let failure_event = Event::workflow_run_failed_from_error( error, - crate::millis_u64(duration), + fabro_types::RunTiming::wall_only(crate::millis_u64(duration)), reason, None, None, @@ -1003,7 +1003,7 @@ impl Drop for DetachedRunBootstrapGuard { handle.spawn(async move { let failure_event = Event::workflow_run_failed_from_error( &Error::engine(reason.to_string()), - 0, + fabro_types::RunTiming::default(), reason, None, None, @@ -1070,7 +1070,7 @@ impl Drop for DetachedRunCompletionGuard { handle.spawn(async move { let failure_event = Event::workflow_run_failed_from_error( &Error::engine(message.to_string()), - 0, + fabro_types::RunTiming::default(), reason, None, None, @@ -1100,8 +1100,15 @@ async fn persist_detached_failure( ) -> Result<(), Error> { let message = error.to_string(); - let failure_event = - Event::workflow_run_failed_from_error(error, 0, reason, None, None, None, None); + let failure_event = Event::workflow_run_failed_from_error( + error, + fabro_types::RunTiming::default(), + reason, + None, + None, + None, + None, + ); if let Err(err) = append_event_to_sink(event_sink, &run_id, &failure_event).await { tracing::warn!(error = %err, "Failed to append detached failure event"); } @@ -1719,7 +1726,7 @@ reasoning = false let conclusion = crate::records::Conclusion { timestamp: Utc::now(), status: StageOutcome::Succeeded, - duration_ms: 1, + timing: fabro_types::RunTiming::wall_only(1), failure: None, final_git_commit_sha: None, stages: vec![], @@ -1761,7 +1768,7 @@ reasoning = false .await .unwrap(); crate::event::append_event(&run_store, &fixtures::RUN_1, &Event::WorkflowRunCompleted { - duration_ms: conclusion.duration_ms, + timing: conclusion.timing, artifact_count: 0, status: "succeeded".to_string(), reason: crate::run_status::SuccessReason::Completed, diff --git a/lib/crates/fabro-workflow/src/pipeline/execute.rs b/lib/crates/fabro-workflow/src/pipeline/execute.rs index 70b826a51..6ae8cc244 100644 --- a/lib/crates/fabro-workflow/src/pipeline/execute.rs +++ b/lib/crates/fabro-workflow/src/pipeline/execute.rs @@ -143,7 +143,7 @@ pub async fn execute(init: Initialized) -> Executed { graph, outcome: Err(err), run_options, - duration_ms: crate::millis_u64(start.elapsed()), + wall_time_ms: crate::millis_u64(start.elapsed()), final_context: seed_context_from_checkpoint(checkpoint.as_ref()), engine, model, @@ -163,7 +163,7 @@ pub async fn execute(init: Initialized) -> Executed { graph, outcome: Err(err), run_options, - duration_ms: crate::millis_u64(start.elapsed()), + wall_time_ms: crate::millis_u64(start.elapsed()), final_context: seed, engine, model, @@ -178,7 +178,7 @@ pub async fn execute(init: Initialized) -> Executed { graph, outcome: Err(err), run_options, - duration_ms: crate::millis_u64(start.elapsed()), + wall_time_ms: crate::millis_u64(start.elapsed()), final_context: Context::new(), engine, model, @@ -294,13 +294,13 @@ pub async fn execute(init: Initialized) -> Executed { engine.registry.shutdown_all(&engine.run.emitter).await; - let duration_ms = crate::millis_u64(start.elapsed()); + let wall_time_ms = crate::millis_u64(start.elapsed()); Executed { graph, outcome, run_options, - duration_ms, + wall_time_ms, final_context, engine, model, diff --git a/lib/crates/fabro-workflow/src/pipeline/finalize.rs b/lib/crates/fabro-workflow/src/pipeline/finalize.rs index a7f4619db..35fdf1d3f 100644 --- a/lib/crates/fabro-workflow/src/pipeline/finalize.rs +++ b/lib/crates/fabro-workflow/src/pipeline/finalize.rs @@ -72,7 +72,7 @@ pub(crate) async fn build_conclusion_from_store( run_store: &RunStoreHandle, status: StageOutcome, failure: Option, - run_duration_ms: u64, + run_wall_time_ms: u64, final_git_commit_sha: Option, ) -> Conclusion { let projection = run_store.state().await.ok(); @@ -94,7 +94,7 @@ pub(crate) async fn build_conclusion_from_store( &projection_order, status, failure, - run_duration_ms, + run_wall_time_ms, final_git_commit_sha, ) } @@ -105,7 +105,7 @@ fn build_conclusion_from_parts( projection_order: &HashMap, status: StageOutcome, failure: Option, - run_duration_ms: u64, + run_wall_time_ms: u64, final_git_commit_sha: Option, ) -> Conclusion { // Looping workflows revisit nodes; `completed_nodes` accumulates duplicates @@ -154,7 +154,8 @@ fn build_conclusion_from_parts( let summary = StageSummary { stage_id: node_id.to_string(), stage_label: node_id.to_string(), - duration_ms: billing.map_or(0, |stage| stage.duration_ms), + timing: billing + .map_or_else(fabro_types::StageTiming::default, |stage| stage.timing), billing_usd_micros: billing.and_then(|stage| stage.billing.total_usd_micros), retries, }; @@ -182,7 +183,7 @@ fn build_conclusion_from_parts( Conclusion { timestamp: chrono::Utc::now(), status, - duration_ms: run_duration_ms, + timing: projection_billing.timing.with_wall_time(run_wall_time_ms), failure, final_git_commit_sha, stages, @@ -443,7 +444,7 @@ pub(crate) fn billing_from_projection(projection: &RunProjection) -> Option, - duration_ms: u64, + timing: fabro_types::RunTiming, artifact_count: usize, final_git_commit_sha: Option, final_patch: Option, @@ -462,7 +463,7 @@ pub(crate) fn build_terminal_event( { let total_usd_micros = billing.as_ref().and_then(|b| b.total_usd_micros); return Event::WorkflowRunCompleted { - duration_ms, + timing, artifact_count, status: outcome_status.to_string(), reason: match outcome_status { @@ -493,7 +494,7 @@ pub(crate) fn build_terminal_event( }; Event::WorkflowRunFailed { failure, - duration_ms, + timing, final_git_commit_sha, final_patch, diff_summary, @@ -533,7 +534,7 @@ pub async fn finalize(executed: Executed, options: &FinalizeOptions) -> Result Result Result, run_options: RunOptions, - duration_ms: u64, + wall_time_ms: u64, services: Arc, ) -> Executed { let mut engine = EngineServices::test_default(); @@ -707,7 +708,7 @@ mod tests { graph, outcome, run_options, - duration_ms, + wall_time_ms, final_context: Context::new(), engine: Arc::new(engine), model: "test-model".to_string(), @@ -955,7 +956,7 @@ mod tests { let failed_usage = test_usage("gpt-old", 100, 10); let success_usage = test_usage("gpt-new", 200, 20); let failed = projection.stage_entry("verify", 1, first_event_seq(1)); - failed.duration_ms = Some(1200); + failed.timing = Some(fabro_types::StageTiming::wall_only(1200)); failed.usage = BilledTokenCounts::from_billed_usage(std::slice::from_ref(&failed_usage)); failed.model = Some(failed_usage.model().clone()); failed.completion = Some(StageCompletion { @@ -967,7 +968,7 @@ mod tests { timestamp: chrono::Utc::now(), }); let succeeded = projection.stage_entry("verify", 2, first_event_seq(2)); - succeeded.duration_ms = Some(800); + succeeded.timing = Some(fabro_types::StageTiming::wall_only(800)); succeeded.usage = BilledTokenCounts::from_billed_usage(std::slice::from_ref(&success_usage)); succeeded.model = Some(success_usage.model().clone()); @@ -982,7 +983,7 @@ mod tests { let projection_billing = billing_rollup_from_projection(&projection, None); let mut latest_outcome = Outcome::success(); latest_outcome.usage = Some(success_usage); - latest_outcome.duration_ms = Some(800); + latest_outcome.timing = Some(fabro_types::StageTiming::wall_only(800)); let mut checkpoint = checkpoint_with( vec!["verify", "verify"], HashMap::from([("verify".to_string(), latest_outcome)]), @@ -1007,7 +1008,7 @@ mod tests { ); assert_eq!(conclusion.stages.len(), 1); assert_eq!(conclusion.stages[0].stage_id, "verify"); - assert_eq!(conclusion.stages[0].duration_ms, 2000); + assert_eq!(conclusion.stages[0].timing.wall_time_ms, 2000); assert_eq!(conclusion.stages[0].billing_usd_micros, Some(330)); assert_eq!(conclusion.stages[0].retries, 1); } @@ -1104,7 +1105,7 @@ mod tests { let conclusion = Conclusion { timestamp: chrono::Utc::now(), status: StageOutcome::Succeeded, - duration_ms: 10, + timing: fabro_types::RunTiming::wall_only(10), failure: None, final_git_commit_sha: None, stages: Vec::new(), @@ -1167,7 +1168,7 @@ mod tests { let conclusion = Conclusion { timestamp: chrono::Utc::now(), status: StageOutcome::Succeeded, - duration_ms: 10, + timing: fabro_types::RunTiming::wall_only(10), failure: None, final_git_commit_sha: None, stages: Vec::new(), @@ -1219,7 +1220,7 @@ mod tests { let conclusion = Conclusion { timestamp: chrono::Utc::now(), status: StageOutcome::Succeeded, - duration_ms: 10, + timing: fabro_types::RunTiming::wall_only(10), failure: None, final_git_commit_sha: None, stages: Vec::new(), diff --git a/lib/crates/fabro-workflow/src/pipeline/pull_request.rs b/lib/crates/fabro-workflow/src/pipeline/pull_request.rs index 2f2518095..a04f6ab8c 100644 --- a/lib/crates/fabro-workflow/src/pipeline/pull_request.rs +++ b/lib/crates/fabro-workflow/src/pipeline/pull_request.rs @@ -172,7 +172,7 @@ fn format_arc_details_section( parts.push(String::new()); // Cost table - let total_duration = format_duration_ms(conclusion.duration_ms); + let total_duration = format_duration_ms(conclusion.timing.wall_time_ms); let total_cost_str = format_cost(conclusion.billing.as_ref().and_then(|b| b.total_usd_micros)); let stage_count = conclusion.stages.len(); parts.push(format!( @@ -184,7 +184,7 @@ fn format_arc_details_section( parts.push("| Stage | Duration | Cost | Retries |".to_string()); parts.push("|---|---|---|---|".to_string()); for stage in &conclusion.stages { - let dur = format_duration_ms(stage.duration_ms); + let dur = format_duration_ms(stage.timing.wall_time_ms); let cost = format_cost(stage.billing_usd_micros); parts.push(format!( "| {} | {} | {} | {} |", @@ -870,28 +870,28 @@ mod tests { Conclusion { timestamp: Utc::now(), status: crate::outcome::StageOutcome::Succeeded, - duration_ms: 150_000, + timing: fabro_types::RunTiming::wall_only(150_000), failure: None, final_git_commit_sha: None, stages: vec![ StageSummary { stage_id: "plan".to_string(), stage_label: "plan".to_string(), - duration_ms: 45_000, + timing: fabro_types::StageTiming::wall_only(45_000), billing_usd_micros: Some(120_000), retries: 0, }, StageSummary { stage_id: "implement".to_string(), stage_label: "implement".to_string(), - duration_ms: 90_000, + timing: fabro_types::StageTiming::wall_only(90_000), billing_usd_micros: Some(250_000), retries: 0, }, StageSummary { stage_id: "simplify".to_string(), stage_label: "simplify".to_string(), - duration_ms: 15_000, + timing: fabro_types::StageTiming::wall_only(15_000), billing_usd_micros: Some(50_000), retries: 0, }, @@ -1244,7 +1244,7 @@ mod tests { node_id: "plan".to_string(), name: "plan".to_string(), index: 0, - duration_ms: 1, + timing: fabro_types::StageTiming::wall_only(1), status: "succeeded".to_string(), preferred_label: None, suggested_next_ids: vec![], @@ -1600,7 +1600,7 @@ mod tests { .await .unwrap(); append_event(&run_store, &fixtures::RUN_1, &Event::WorkflowRunCompleted { - duration_ms: 1, + timing: fabro_types::RunTiming::wall_only(1), artifact_count: 0, status: "succeeded".to_string(), reason: SuccessReason::Completed, @@ -1717,7 +1717,7 @@ mod tests { node_id: "plan".to_string(), name: "plan".to_string(), index: 0, - duration_ms: 1, + timing: fabro_types::StageTiming::wall_only(1), status: "succeeded".to_string(), preferred_label: None, suggested_next_ids: vec![], @@ -1888,7 +1888,7 @@ mod tests { .await .unwrap(); append_event(&run_store, &fixtures::RUN_1, &Event::WorkflowRunCompleted { - duration_ms: 1, + timing: fabro_types::RunTiming::wall_only(1), artifact_count: 0, status: "succeeded".to_string(), reason: SuccessReason::Completed, diff --git a/lib/crates/fabro-workflow/src/pipeline/types.rs b/lib/crates/fabro-workflow/src/pipeline/types.rs index 2398441f1..39085a904 100644 --- a/lib/crates/fabro-workflow/src/pipeline/types.rs +++ b/lib/crates/fabro-workflow/src/pipeline/types.rs @@ -299,7 +299,8 @@ pub struct Executed { pub graph: Graph, pub outcome: Result, pub run_options: RunOptions, - pub duration_ms: u64, + /// Run wall-clock time in milliseconds from EXECUTE start to outcome. + pub wall_time_ms: u64, pub final_context: Context, pub engine: Arc, pub model: String, diff --git a/lib/crates/fabro-workflow/src/run_lookup.rs b/lib/crates/fabro-workflow/src/run_lookup.rs index 40683e230..176bbc144 100644 --- a/lib/crates/fabro-workflow/src/run_lookup.rs +++ b/lib/crates/fabro-workflow/src/run_lookup.rs @@ -140,10 +140,10 @@ impl RunInfo { } } - pub fn duration_ms(&self) -> Option { + pub fn wall_time_ms(&self) -> Option { self.summary .as_ref() - .and_then(|summary| summary.timestamps.duration_ms) + .and_then(|summary| summary.timing.as_ref().map(|t| t.wall_time_ms)) } pub fn total_cost(&self) -> Option { @@ -284,8 +284,11 @@ fn run_info_from_summary(summary: &Run, scratch_base: &Path) -> Option let dir_name = path.file_name()?.to_string_lossy().to_string(); let start_time_dt = summary.id.created_at(); let end_time = if summary.lifecycle.status.is_terminal() { - summary.timestamps.duration_ms.and_then(|duration_ms| { - Some(start_time_dt + chrono::Duration::milliseconds(i64::try_from(duration_ms).ok()?)) + summary.timing.as_ref().and_then(|timing| { + Some( + start_time_dt + + chrono::Duration::milliseconds(i64::try_from(timing.wall_time_ms).ok()?), + ) }) } else { None diff --git a/lib/crates/fabro-workflow/src/test_support.rs b/lib/crates/fabro-workflow/src/test_support.rs index 75a999e40..aa9c8117a 100644 --- a/lib/crates/fabro-workflow/src/test_support.rs +++ b/lib/crates/fabro-workflow/src/test_support.rs @@ -40,7 +40,7 @@ async fn execute_and_emit_terminal(initialized: InitializedState) -> Executed { let billing = state.as_ref().and_then(billing_from_projection); let event = build_terminal_event( &executed.outcome, - executed.duration_ms, + fabro_types::RunTiming::wall_only(executed.wall_time_ms), 0, None, None, diff --git a/lib/crates/fabro-workflow/tests/it/integration.rs b/lib/crates/fabro-workflow/tests/it/integration.rs index 59ecd87b3..13c3c1900 100644 --- a/lib/crates/fabro-workflow/tests/it/integration.rs +++ b/lib/crates/fabro-workflow/tests/it/integration.rs @@ -7130,7 +7130,7 @@ async fn workflow_run_with_vault_only_openai_codex_builds_pr_body() { Some(&Conclusion { timestamp: Utc::now(), status: StageOutcome::Succeeded, - duration_ms: 1, + timing: fabro_types::RunTiming::wall_only(1), failure: None, final_git_commit_sha: None, stages: Vec::new(), diff --git a/lib/packages/fabro-api-client/package.json b/lib/packages/fabro-api-client/package.json index ae257ba1a..7709ed33a 100644 --- a/lib/packages/fabro-api-client/package.json +++ b/lib/packages/fabro-api-client/package.json @@ -8,6 +8,7 @@ "typecheck": "tsc" }, "devDependencies": { + "@openapitools/openapi-generator-cli": "2.20.2", "typescript": "^5.9.2" }, "dependencies": { diff --git a/lib/packages/fabro-api-client/src/.openapi-generator/FILES b/lib/packages/fabro-api-client/src/.openapi-generator/FILES index 0954e48c3..378a74e29 100644 --- a/lib/packages/fabro-api-client/src/.openapi-generator/FILES +++ b/lib/packages/fabro-api-client/src/.openapi-generator/FILES @@ -332,7 +332,7 @@ models/run-status-succeeded.ts models/run-status.ts models/run-superseded-by-props.ts models/run-timestamps.ts -models/run-timings.ts +models/run-timing.ts models/run.ts models/sandbox-details.ts models/sandbox-file-entry.ts @@ -384,6 +384,7 @@ models/stage-outcome.ts models/stage-projection.ts models/stage-state.ts models/stage-summary.ts +models/stage-timing.ts models/start-record.ts models/start-run-request.ts models/steer-run-request.ts @@ -425,4 +426,4 @@ models/workflow-ref.ts models/workflow-reference.ts models/workflow-schedule-summary.ts models/workflow-settings.ts -models/write-blob-response.ts +models/write-blob-response.ts \ No newline at end of file diff --git a/lib/packages/fabro-api-client/src/models/aggregate-billing-totals.ts b/lib/packages/fabro-api-client/src/models/aggregate-billing-totals.ts index bed219431..505bdf57a 100644 --- a/lib/packages/fabro-api-client/src/models/aggregate-billing-totals.ts +++ b/lib/packages/fabro-api-client/src/models/aggregate-billing-totals.ts @@ -13,6 +13,9 @@ */ +// May contain unused imports in some cases +// @ts-ignore +import type { RunTiming } from './run-timing'; /** * Aggregate billing totals across all runs. @@ -51,7 +54,7 @@ export interface AggregateBillingTotals { */ 'total_usd_micros'?: number | null; /** - * Total runtime in seconds. + * Aggregate timing rollup across every completed run. Active timing sums work across stage visits, so `active_time_ms` can exceed `wall_time_ms`. */ - 'runtime_secs': number; + 'timing': RunTiming; } diff --git a/lib/packages/fabro-api-client/src/models/check-run.ts b/lib/packages/fabro-api-client/src/models/check-run.ts index 9ea61ee30..b94d2b9e5 100644 --- a/lib/packages/fabro-api-client/src/models/check-run.ts +++ b/lib/packages/fabro-api-client/src/models/check-run.ts @@ -27,7 +27,7 @@ export interface CheckRun { 'name': string; 'status': CheckRunStatus; /** - * Duration of the check run in seconds. + * Wall-clock duration of the check run in milliseconds. */ - 'duration_secs'?: number; + 'wall_time_ms'?: number; } diff --git a/lib/packages/fabro-api-client/src/models/conclusion.ts b/lib/packages/fabro-api-client/src/models/conclusion.ts index b25c1b32f..f687ddf24 100644 --- a/lib/packages/fabro-api-client/src/models/conclusion.ts +++ b/lib/packages/fabro-api-client/src/models/conclusion.ts @@ -24,6 +24,9 @@ import type { RunDiff } from './run-diff'; import type { RunFailure } from './run-failure'; // May contain unused imports in some cases // @ts-ignore +import type { RunTiming } from './run-timing'; +// May contain unused imports in some cases +// @ts-ignore import type { StageOutcome } from './stage-outcome'; // May contain unused imports in some cases // @ts-ignore @@ -35,7 +38,7 @@ import type { StageSummary } from './stage-summary'; export interface Conclusion { 'timestamp': string; 'status': StageOutcome; - 'duration_ms': number; + 'timing': RunTiming; 'failure'?: RunFailure | null; 'final_git_commit_sha'?: string | null; 'stages': Array; diff --git a/lib/packages/fabro-api-client/src/models/index.ts b/lib/packages/fabro-api-client/src/models/index.ts index ee5532b9a..9c5cd2123 100644 --- a/lib/packages/fabro-api-client/src/models/index.ts +++ b/lib/packages/fabro-api-client/src/models/index.ts @@ -309,7 +309,7 @@ export * from './run-status-submitted'; export * from './run-status-succeeded'; export * from './run-superseded-by-props'; export * from './run-timestamps'; -export * from './run-timings'; +export * from './run-timing'; export * from './sandbox-details'; export * from './sandbox-file-entry'; export * from './sandbox-file-list-response'; @@ -360,6 +360,7 @@ export * from './stage-outcome'; export * from './stage-projection'; export * from './stage-state'; export * from './stage-summary'; +export * from './stage-timing'; export * from './start-record'; export * from './start-run-request'; export * from './steer-run-request'; @@ -401,4 +402,4 @@ export * from './workflow-ref'; export * from './workflow-reference'; export * from './workflow-schedule-summary'; export * from './workflow-settings'; -export * from './write-blob-response'; +export * from './write-blob-response'; \ No newline at end of file diff --git a/lib/packages/fabro-api-client/src/models/run-billing-stage.ts b/lib/packages/fabro-api-client/src/models/run-billing-stage.ts index 5cef24796..6238e97c8 100644 --- a/lib/packages/fabro-api-client/src/models/run-billing-stage.ts +++ b/lib/packages/fabro-api-client/src/models/run-billing-stage.ts @@ -25,18 +25,21 @@ import type { BillingStageRef } from './billing-stage-ref'; // May contain unused imports in some cases // @ts-ignore import type { StageState } from './stage-state'; +// May contain unused imports in some cases +// @ts-ignore +import type { StageTiming } from './stage-timing'; /** - * Token counts and billed totals for one workflow node within a run. Rows are grouped by node; billing and runtime sum every visit of that node. + * Token counts and billed totals for one workflow node within a run. Rows are grouped by node; billing and timing sum every visit of that node. */ export interface RunBillingStage { 'stage': BillingStageRef; 'model': BillingModelRef | null; 'billing': BilledTokenCounts; /** - * Wall-clock runtime in seconds, summed across every visit of this node. + * Per-node timing summed across every visit. `wall_time_ms` is the sum of visit wall times; the active breakdown sums work timing. */ - 'runtime_secs': number; + 'timing': StageTiming; /** * Wall-clock time the latest attempt of this stage started, if known. */ diff --git a/lib/packages/fabro-api-client/src/models/run-billing-totals.ts b/lib/packages/fabro-api-client/src/models/run-billing-totals.ts index f38594452..68861f7f8 100644 --- a/lib/packages/fabro-api-client/src/models/run-billing-totals.ts +++ b/lib/packages/fabro-api-client/src/models/run-billing-totals.ts @@ -13,15 +13,18 @@ */ +// May contain unused imports in some cases +// @ts-ignore +import type { RunTiming } from './run-timing'; /** * Aggregate billing totals across all stages of a run. */ export interface RunBillingTotals { /** - * Total wall-clock runtime in seconds. + * Run-level timing rollup. `wall_time_ms` is summed across stage visits; active timing sums work across visits. */ - 'runtime_secs': number; + 'timing': RunTiming; /** * Total input tokens consumed. */ diff --git a/lib/packages/fabro-api-client/src/models/run-stage.ts b/lib/packages/fabro-api-client/src/models/run-stage.ts index 966d41d78..7c95a4853 100644 --- a/lib/packages/fabro-api-client/src/models/run-stage.ts +++ b/lib/packages/fabro-api-client/src/models/run-stage.ts @@ -35,9 +35,9 @@ export interface RunStage { 'handler': StageHandler; 'status': StageState; /** - * Time spent in this stage, in seconds. + * Wall-clock time the latest attempt spent in this stage, in milliseconds. */ - 'duration_secs'?: number; + 'wall_time_ms'?: number; /** * Node id in the workflow graph; multiple stages with different visits share the same node_id. */ diff --git a/lib/packages/fabro-api-client/src/models/run-timestamps.ts b/lib/packages/fabro-api-client/src/models/run-timestamps.ts index 5debf5a4a..abb59f410 100644 --- a/lib/packages/fabro-api-client/src/models/run-timestamps.ts +++ b/lib/packages/fabro-api-client/src/models/run-timestamps.ts @@ -19,6 +19,4 @@ export interface RunTimestamps { 'started_at': string | null; 'last_event_at': string | null; 'completed_at': string | null; - 'duration_ms'?: number | null; - 'elapsed_secs'?: number | null; } diff --git a/lib/packages/fabro-api-client/src/models/run-timings.ts b/lib/packages/fabro-api-client/src/models/run-timing.ts similarity index 50% rename from lib/packages/fabro-api-client/src/models/run-timings.ts rename to lib/packages/fabro-api-client/src/models/run-timing.ts index a70b29049..fb585b0e9 100644 --- a/lib/packages/fabro-api-client/src/models/run-timings.ts +++ b/lib/packages/fabro-api-client/src/models/run-timing.ts @@ -15,15 +15,14 @@ /** - * Timing information for a run. + * Timing rollup for an entire run. Active fields sum work across stage visits, so `active_time_ms` can exceed `wall_time_ms` when parallel branches run concurrently. */ -export interface RunTimings { +export interface RunTiming { + 'wall_time_ms': number; + 'inference_time_ms'?: number; + 'tool_time_ms'?: number; /** - * Wall-clock time elapsed in seconds. + * Equals `inference_time_ms + tool_time_ms`. */ - 'elapsed_secs': number; - /** - * Whether the elapsed time exceeds the expected threshold. - */ - 'elapsed_warning'?: boolean; + 'active_time_ms': number; } diff --git a/lib/packages/fabro-api-client/src/models/run.ts b/lib/packages/fabro-api-client/src/models/run.ts index c95cd5d36..ee2b63081 100644 --- a/lib/packages/fabro-api-client/src/models/run.ts +++ b/lib/packages/fabro-api-client/src/models/run.ts @@ -54,6 +54,9 @@ import type { RunSandbox } from './run-sandbox'; import type { RunTimestamps } from './run-timestamps'; // May contain unused imports in some cases // @ts-ignore +import type { RunTiming } from './run-timing'; +// May contain unused imports in some cases +// @ts-ignore import type { WorkflowRef } from './workflow-ref'; /** @@ -82,6 +85,7 @@ export interface Run { 'models': Array; 'source_directory': string | null; 'timestamps': RunTimestamps; + 'timing': RunTiming | null; 'billing': RunBillingSummary | null; 'diff': DiffSummary | null; 'pull_request': PullRequestLink | null; diff --git a/lib/packages/fabro-api-client/src/models/stage-projection.ts b/lib/packages/fabro-api-client/src/models/stage-projection.ts index 7021a8f9f..f982af2d6 100644 --- a/lib/packages/fabro-api-client/src/models/stage-projection.ts +++ b/lib/packages/fabro-api-client/src/models/stage-projection.ts @@ -28,6 +28,9 @@ import type { StageCompletion } from './stage-completion'; // May contain unused imports in some cases // @ts-ignore import type { StageState } from './stage-state'; +// May contain unused imports in some cases +// @ts-ignore +import type { StageTiming } from './stage-timing'; /** * Observable projection data for one workflow stage execution. @@ -62,10 +65,7 @@ export interface StageProjection { * Wall-clock time the latest attempt of this stage started, if known. */ 'started_at'?: string | null; - /** - * Wall-clock duration of the stage\'s latest terminal attempt, if known. - */ - 'duration_ms'?: number | null; + 'timing'?: StageTiming | null; 'usage': BilledTokenCounts; 'model'?: BillingModelRef | null; /** diff --git a/lib/packages/fabro-api-client/src/models/stage-summary.ts b/lib/packages/fabro-api-client/src/models/stage-summary.ts index 527268c3f..011fcf555 100644 --- a/lib/packages/fabro-api-client/src/models/stage-summary.ts +++ b/lib/packages/fabro-api-client/src/models/stage-summary.ts @@ -13,6 +13,9 @@ */ +// May contain unused imports in some cases +// @ts-ignore +import type { StageTiming } from './stage-timing'; /** * Terminal summary for one stage in a run conclusion. @@ -20,7 +23,7 @@ export interface StageSummary { 'stage_id': string; 'stage_label': string; - 'duration_ms': number; + 'timing': StageTiming; 'billing_usd_micros'?: number | null; 'retries': number; } diff --git a/lib/packages/fabro-api-client/src/models/stage-timing.ts b/lib/packages/fabro-api-client/src/models/stage-timing.ts new file mode 100644 index 000000000..d9915c409 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/stage-timing.ts @@ -0,0 +1,28 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +/** + * Timing breakdown for one stage visit. Fields are all milliseconds. `wall_time_ms` is elapsed clock time; `inference_time_ms` is Fabro- observed LLM request/stream elapsed time; `tool_time_ms` is tool or command execution elapsed time; `active_time_ms` equals `inference_time_ms + tool_time_ms`. + */ +export interface StageTiming { + 'wall_time_ms': number; + 'inference_time_ms'?: number; + 'tool_time_ms'?: number; + /** + * Equals `inference_time_ms + tool_time_ms`. + */ + 'active_time_ms': number; +}