Compare commits

...
Author SHA1 Message Date
Diego Imbert 2fe81b2445 Merge branch 'main' into workflow-execution-time-display 2026-06-09 19:12:53 +02:00
Diego Imbert 370c7439fe Merge branch 'main' into workflow-execution-time-display 2026-06-09 08:14:18 +02:00
Diego ImbertandClaude Opus 4.8 dbc80e671c fix: show wall-clock execution time for WAC roots in the UI, keep duration_ms as worker time
Reworks the workflow-as-code (WAC) execution-time fix. The earlier approach
persisted wall-clock into `v2_job_completed.duration_ms`, but that column is
read as worker *service time* by cloud-usage accounting and EE workspace
fairness — so a WAC root that suspends while its task jobs run would bill and
be throttled for idle wall-clock, and counting it any other way (e.g. excluding
it) would let arbitrary user code in the root go uncounted.

Instead, keep `duration_ms` as the worker-measured value (revert the backend
change entirely) and compute the wall-clock total in the UI from
`completed_at - started_at` for WAC roots only. WAC roots are identified by the
`_checkpoint` in `workflow_as_code_status` (present in the completed-job API
payload; AI-agent jobs populate the column but have no `_checkpoint`).

- frontend/src/lib/utils.ts: add `isWorkflowAsCodeRoot` + `jobDisplayDurationMs`
- JobStatus.svelte / JobPreview.svelte: render the WAC-aware display duration

Reverts the duration_ms/test/sqlx/ee-repo-ref changes from the prior commits so
the backend is unchanged vs main; no EE companion change is needed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 15:21:40 +02:00
Diego ImbertandClaude Opus 4.8 0985d6b7b4 fix(backend): account worker-measured duration for WAC roots, not wall-clock
The display fix made WAC roots persist wall-clock `duration_ms` (they suspend
while their task jobs run). That column also feeds cloud usage accounting, so
a suspended WAC root was billing idle wall-clock on top of its child task jobs.
Account the worker-measured `$9` duration instead (captured before it is
shadowed by the persisted value); behaviour is unchanged for all non-WAC jobs.

Bumps ee-repo-ref.txt for the companion EE change excluding WAC roots from
workspace fairness service-time sampling.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 14:44:53 +02:00
Diego Imbert c1a8250340 Merge branch 'main' into workflow-execution-time-display 2026-06-04 17:58:31 +02:00
Diego ImbertandClaude Opus 4.8 83cd26ee2e fix(backend): report wall-clock duration for workflow-as-code roots
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 20:11:22 +02:00
3 changed files with 48 additions and 4 deletions
+3 -3
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import { displayDate, msToReadableTime } from '$lib/utils'
import { displayDate, jobDisplayDurationMs, msToReadableTime } from '$lib/utils'
import type { CompletedJob, QueuedJob } from '$lib/gen'
import Badge from './common/badge/Badge.svelte'
import { forLater } from '$lib/forLater'
@@ -20,7 +20,7 @@
{#if job && 'success' in job && job.success}
<Badge {large} color="green">
Successfully ran in {msToReadableTime(job.duration_ms)}
Successfully ran in {msToReadableTime(jobDisplayDurationMs(job))}
{job.is_skipped ? '(Skipped)' : ''}
{#if job.self_wait_time_ms || job.aggregate_wait_time_ms}
<WaitTimeWarning
@@ -32,7 +32,7 @@
</Badge>
{:else if job && 'success' in job}
<Badge {large} color="red">
Failed after {msToReadableTime(job.duration_ms)}
Failed after {msToReadableTime(jobDisplayDurationMs(job))}
{#if job.self_wait_time_ms || job.aggregate_wait_time_ms}
<WaitTimeWarning
self_wait_time_ms={job.self_wait_time_ms}
@@ -15,6 +15,7 @@
import { Badge } from '../common'
import { forLater } from '$lib/forLater'
import DurationMs from '../DurationMs.svelte'
import { jobDisplayDurationMs } from '$lib/utils'
import { workspaceStore } from '$lib/stores'
import { twMerge } from 'tailwind-merge'
@@ -157,7 +158,7 @@
</Badge>
{#if job?.['duration_ms']}
<DurationMs
duration_ms={job?.['duration_ms']}
duration_ms={jobDisplayDurationMs(job) ?? job?.['duration_ms']}
self_wait_time_ms={job?.self_wait_time_ms}
aggregate_wait_time_ms={job?.aggregate_wait_time_ms}
/>
+43
View File
@@ -239,6 +239,49 @@ export function msToReadableTime(ms: number | undefined, maximumFractionDigits?:
}
}
/**
* A workflow-as-code (WAC) root is a script/preview job whose
* `workflow_as_code_status` carries the `_checkpoint` written by the WAC
* executor when it dispatches task jobs. AI-agent jobs also populate
* `workflow_as_code_status` but never write `_checkpoint`, so they are not
* matched here.
*/
export function isWorkflowAsCodeRoot(
job: { workflow_as_code_status?: unknown } | undefined
): boolean {
const wac = job?.workflow_as_code_status as Record<string, unknown> | undefined
return wac != undefined && wac['_checkpoint'] != undefined
}
/**
* Total execution time to display for a job, in ms.
*
* WAC roots suspend while their task jobs run, so their worker-measured
* `duration_ms` only covers the orchestration script's own compute, not the
* end-to-end run. For those, show the wall-clock span (`completed_at -
* started_at`) — the same total a flow reports. Everything else (and any WAC
* root missing the timestamps) falls back to `duration_ms`.
*/
export function jobDisplayDurationMs(
job:
| {
started_at?: string
completed_at?: string
duration_ms?: number
workflow_as_code_status?: unknown
}
| undefined
): number | undefined {
if (isWorkflowAsCodeRoot(job) && job?.started_at && job?.completed_at) {
const start = new Date(job.started_at).getTime()
const end = new Date(job.completed_at).getTime()
if (isFinite(start) && isFinite(end) && end >= start) {
return end - start
}
}
return job?.duration_ms
}
export function msToReadableTimeShort(
ms: number | undefined,
maximumFractionDigits?: number