Skip to content

feat(core): measure hook-triggered time to resume - #3437

Merged
karthikscale3 merged 1 commit into
mainfrom
kk/hook-resume-ttr-telemetry
Aug 12, 2026
Merged

feat(core): measure hook-triggered time to resume#3437
karthikscale3 merged 1 commit into
mainfrom
kk/hook-resume-ttr-telemetry

Conversation

@karthikscale3

@karthikscale3 karthikscale3 commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Adds SDK-side OTEL telemetry for hook-triggered time-to-resume, so we can see where the latency between resumeHook() and the next step actually goes.

workflow.resume.total_ms — entry into the public resume API → the first line of the next durable step — plus a non-overlapping phase breakdown that sums exactly to it:

producer_prep · queue_delivery · resume_setup · replay · step_dispatch · step_claim · step_prepare

Dimensioned by trigger, strategy (parallel/sequential), setup_source, and step_execution (inline/dispatched). All on the first step's step.execute span — one resumption, one sample.

The boundaries travel on a new optional hookResumeTiming field on the queue message. No workflow-server change; the field is optional in both directions and parses with .catch(undefined) so a malformed value can't fail a delivery. A sample is emitted only when every boundary is present, finite, and monotonic — a clock-skewed set is dropped rather than reported as a negative phase.

@karthikscale3
karthikscale3 requested a review from a team as a code owner August 10, 2026 23:26
@changeset-bot

changeset-bot Bot commented Aug 10, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: ae416cb

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 20 packages
Name Type
@workflow/core Patch
@workflow/world Patch
@workflow/builders Patch
@workflow/cli Patch
@workflow/next Patch
@workflow/nitro Patch
@workflow/vitest Patch
@workflow/web-shared Patch
@workflow/web Patch
workflow Patch
@workflow/world-testing Patch
@workflow/world-local Patch
@workflow/world-postgres Patch
@workflow/world-vercel Patch
@workflow/astro Patch
@workflow/nest Patch
@workflow/rollup Patch
@workflow/sveltekit Patch
@workflow/vite Patch
@workflow/nuxt Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
example-nextjs-workflow-turbopack Ready Ready Preview Aug 12, 2026 6:03pm
example-nextjs-workflow-webpack Ready Ready Preview Aug 12, 2026 6:03pm
example-workflow Ready Ready Preview Aug 12, 2026 6:03pm
workbench-astro-workflow Ready Ready Preview Aug 12, 2026 6:03pm
workbench-express-workflow Ready Ready Preview Aug 12, 2026 6:03pm
workbench-fastify-workflow Ready Ready Preview Aug 12, 2026 6:03pm
workbench-hono-workflow Ready Ready Preview Aug 12, 2026 6:03pm
workbench-nestjs-workflow Ready Ready Preview Aug 12, 2026 6:03pm
workbench-nitro-workflow Ready Ready Preview Aug 12, 2026 6:03pm
workbench-nuxt-workflow Ready Ready Preview Aug 12, 2026 6:03pm
workbench-python-workflow Error Error Aug 12, 2026 6:03pm
workbench-sveltekit-workflow Ready Ready Preview Aug 12, 2026 6:03pm
workbench-tanstack-start-workflow Ready Ready Preview Aug 12, 2026 6:03pm
workbench-vite-workflow Ready Ready Preview Aug 12, 2026 6:03pm
workflow-docs Ready Ready Preview, v0 Aug 12, 2026 6:03pm
workflow-swc-playground Ready Ready Preview Aug 12, 2026 6:03pm
workflow-tarballs Ready Ready Preview Aug 12, 2026 6:03pm
workflow-web Ready Ready Preview Aug 12, 2026 6:03pm

@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

❌ Failed E2E Tests

🐘 Local Postgres (1 failed)

nextjs-webpack-stable-quickjs (1 failed):

  • distributedAbortController - TTL expiration triggers signal | wrun_41KZVKC1WD0GWR04ZE2KTFTQF0

E2E Test Summary

Summary
Passed Failed Skipped Total
✅ ▲ Vercel Production 3466 0 590 4056
✅ 💻 Local Development 3810 0 558 4368
✅ 📦 Local Production 3810 0 558 4368
❌ 🐘 Local Postgres 3809 1 558 4368
✅ 🪟 Windows 156 0 0 156
✅ vercel-multi-region 27 0 0 27
Total 15078 1 2264 17343
Details by Category

✅ ▲ Vercel Production

App Passed Failed Skipped
✅ astro-node 128 0 28
✅ astro-quickjs 128 0 28
✅ example-node 128 0 28
✅ example-quickjs 128 0 28
✅ express-node 128 0 28
✅ express-quickjs 128 0 28
✅ fastify-node 128 0 28
✅ fastify-quickjs 128 0 28
✅ hono-node 128 0 28
✅ hono-quickjs 128 0 28
✅ nest-node 128 0 28
✅ nest-quickjs 128 0 28
✅ nextjs-turbopack-node 153 0 3
✅ nextjs-turbopack-quickjs 153 0 3
✅ nextjs-webpack-node 153 0 3
✅ nextjs-webpack-quickjs 153 0 3
✅ nitro-node 128 0 28
✅ nitro-quickjs 128 0 28
✅ nuxt-node 128 0 28
✅ nuxt-quickjs 128 0 28
✅ sveltekit-node 147 0 9
✅ sveltekit-quickjs 147 0 9
✅ tanstack-start-node 128 0 28
✅ tanstack-start-quickjs 128 0 28
✅ vite-node 128 0 28
✅ vite-quickjs 128 0 28

✅ 💻 Local Development

App Passed Failed Skipped
✅ astro-stable-node 130 0 26
✅ astro-stable-quickjs 130 0 26
✅ express-stable-node 130 0 26
✅ express-stable-quickjs 130 0 26
✅ fastify-stable-node 130 0 26
✅ fastify-stable-quickjs 130 0 26
✅ hono-stable-node 130 0 26
✅ hono-stable-quickjs 130 0 26
✅ nest-stable-node 130 0 26
✅ nest-stable-quickjs 130 0 26
✅ nextjs-turbopack-canary-node 137 0 19
✅ nextjs-turbopack-canary-quickjs 137 0 19
✅ nextjs-turbopack-stable-node 156 0 0
✅ nextjs-turbopack-stable-quickjs 156 0 0
✅ nextjs-webpack-canary-node 137 0 19
✅ nextjs-webpack-canary-quickjs 137 0 19
✅ nextjs-webpack-stable-node 156 0 0
✅ nextjs-webpack-stable-quickjs 156 0 0
✅ nitro-stable-node 130 0 26
✅ nitro-stable-quickjs 130 0 26
✅ nuxt-stable-node 130 0 26
✅ nuxt-stable-quickjs 130 0 26
✅ sveltekit-stable-node 149 0 7
✅ sveltekit-stable-quickjs 149 0 7
✅ tanstack-start-node 130 0 26
✅ tanstack-start-quickjs 130 0 26
✅ vite-stable-node 130 0 26
✅ vite-stable-quickjs 130 0 26

✅ 📦 Local Production

App Passed Failed Skipped
✅ astro-stable-node 130 0 26
✅ astro-stable-quickjs 130 0 26
✅ express-stable-node 130 0 26
✅ express-stable-quickjs 130 0 26
✅ fastify-stable-node 130 0 26
✅ fastify-stable-quickjs 130 0 26
✅ hono-stable-node 130 0 26
✅ hono-stable-quickjs 130 0 26
✅ nest-stable-node 130 0 26
✅ nest-stable-quickjs 130 0 26
✅ nextjs-turbopack-canary-node 137 0 19
✅ nextjs-turbopack-canary-quickjs 137 0 19
✅ nextjs-turbopack-stable-node 156 0 0
✅ nextjs-turbopack-stable-quickjs 156 0 0
✅ nextjs-webpack-canary-node 137 0 19
✅ nextjs-webpack-canary-quickjs 137 0 19
✅ nextjs-webpack-stable-node 156 0 0
✅ nextjs-webpack-stable-quickjs 156 0 0
✅ nitro-stable-node 130 0 26
✅ nitro-stable-quickjs 130 0 26
✅ nuxt-stable-node 130 0 26
✅ nuxt-stable-quickjs 130 0 26
✅ sveltekit-stable-node 149 0 7
✅ sveltekit-stable-quickjs 149 0 7
✅ tanstack-start-node 130 0 26
✅ tanstack-start-quickjs 130 0 26
✅ vite-stable-node 130 0 26
✅ vite-stable-quickjs 130 0 26

❌ 🐘 Local Postgres

App Passed Failed Skipped
✅ astro-stable-node 130 0 26
✅ astro-stable-quickjs 130 0 26
✅ express-stable-node 130 0 26
✅ express-stable-quickjs 130 0 26
✅ fastify-stable-node 130 0 26
✅ fastify-stable-quickjs 130 0 26
✅ hono-stable-node 130 0 26
✅ hono-stable-quickjs 130 0 26
✅ nest-stable-node 130 0 26
✅ nest-stable-quickjs 130 0 26
✅ nextjs-turbopack-canary-node 137 0 19
✅ nextjs-turbopack-canary-quickjs 137 0 19
✅ nextjs-turbopack-stable-node 156 0 0
✅ nextjs-turbopack-stable-quickjs 156 0 0
✅ nextjs-webpack-canary-node 137 0 19
✅ nextjs-webpack-canary-quickjs 137 0 19
✅ nextjs-webpack-stable-node 156 0 0
❌ nextjs-webpack-stable-quickjs 155 1 0
✅ nitro-stable-node 130 0 26
✅ nitro-stable-quickjs 130 0 26
✅ nuxt-stable-node 130 0 26
✅ nuxt-stable-quickjs 130 0 26
✅ sveltekit-stable-node 149 0 7
✅ sveltekit-stable-quickjs 149 0 7
✅ tanstack-start-node 130 0 26
✅ tanstack-start-quickjs 130 0 26
✅ vite-stable-node 130 0 26
✅ vite-stable-quickjs 130 0 26

✅ 🪟 Windows

App Passed Failed Skipped
✅ nextjs-turbopack-quickjs 156 0 0

✅ vercel-multi-region

App Passed Failed Skipped
✅ nextjs-turbopack 27 0 0

📋 View full workflow run

@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit ae416cb · Wed, 12 Aug 2026 18:19:18 GMT · run logs

Backend: vercel · app: nextjs-turbopack

Metric Scenario Best (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS step 1366 (+34%) 🔻 1479 🔴 (+37%) 🔻 1507 🔴 (+33%) 🔻 1632 🔴 (±0%) 30
TTFS stream 342 (+32%) 🔻 1477 🔴 (+39%) 🔻 1519 🔴 (+39%) 🔻 1594 🔴 (+30%) 🔻 30
TTFS hook + stream 359 (-19%) 💚 1798 🔴 (+30%) 🔻 1864 🔴 (+30%) 🔻 2017 🔴 (+26%) 🔻 30
STSO 1020 steps (inline) 131 (+20%) 🔻 177 (-4.3%) 197 (-8.4%) 330 (-16%) 💚 1019
WO 1020 steps 177797 (-1.6%) 177797 (-1.6%) 177797 (-1.6%) 177797 (-1.6%) 1
SL stream latency 107 (+15%) 🔻 130 🔴 (-12%) 170 🔴 (+10%) 261 🔴 (+20%) 🔻 30
SO stream overhead (text) 119 (+10%) 211 (-7.9%) 328 (+14%) 431 (-55%) 💚 30
SO stream overhead (structured) 120 (-4.8%) 230 (-11%) 341 (+13%) 4228 🔴 (+375%) 🔻 30
📈 STSO distribution vs main (inline / queue-hop histograms)

1020 steps (inline)

Cumulative STSO time: main 179622ms → this run 176355ms (Δ -3267ms, -2%)

100-150 ms  ┃█████                    main 232  this  47  -185
150-200 ms  █████████████████░░░░░░┃  main 622  this 879  +257
200-250 ms  █┃█                       main 111  this  68   -43
250-300 ms  ┃                         main  22  this  13    -9
300-350 ms  ┃                         main  11  this   4    -7
350-400 ms  ┃                         main  11  this   6    -5
400-450 ms  ┃                         main   8  this   0    -8
450-500 ms  ┃                         main   1  this   2    +1
600-650 ms  ┃                         main   1  this   0    -1
📜 Previous results (3)

a27474a

Tue, 11 Aug 2026 19:06:07 GMT · run logs

vercel / nextjs-turbopack

Metric Scenario Best (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS step 270 (-69%) 💚 1470 🔴 (+20%) 🔻 1481 🔴 (+13%) 1619 🔴 (-10%) 30
TTFS stream 232 (-78%) 💚 1348 🔴 (+19%) 🔻 1458 🔴 (+25%) 🔻 1476 🔴 (+19%) 🔻 30
TTFS hook + stream 1550 (+10%) 1715 🔴 (+10%) 1776 🔴 (+12%) 5960 🔴 (+273%) 🔻 30
STSO 1020 steps (inline) 121 (-23%) 💚 159 (-36%) 💚 177 (-43%) 💚 277 (-61%) 💚 1019
WO 1020 steps 159780 (-34%) 💚 159780 (-34%) 💚 159780 (-34%) 💚 159780 (-34%) 💚 1
SL stream latency 83 (-34%) 💚 115 🔴 (-38%) 💚 134 🔴 (-43%) 💚 183 🔴 (-42%) 💚 30
SO stream overhead (text) 105 (-29%) 💚 176 (-29%) 💚 212 (-39%) 💚 3418 🔴 (+475%) 🔻 30
SO stream overhead (structured) 111 (-31%) 💚 164 (-46%) 💚 196 (-44%) 💚 440 (-58%) 💚 30

9b75a61

Tue, 11 Aug 2026 15:43:09 GMT · run logs

vercel / nextjs-turbopack

Metric Scenario Best (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS step 1285 (+22%) 🔻 1349 🔴 (+15%) 1381 🔴 (+15%) 1403 🔴 (-18%) 💚 30
TTFS stream 241 (-77%) 💚 1366 🔴 (+20%) 🔻 1397 🔴 (+20%) 🔻 1453 🔴 (+20%) 🔻 30
TTFS hook + stream 1551 (+24%) 🔻 1649 🔴 (+21%) 🔻 1758 🔴 (+26%) 🔻 1819 🔴 (+21%) 🔻 30
STSO 1020 steps (inline) 102 (+6.3%) 136 (-17%) 💚 158 (-22%) 💚 281 (-64%) 💚 1019
WO 1020 steps 137368 (-20%) 💚 137368 (-20%) 💚 137368 (-20%) 💚 137368 (-20%) 💚 1
SL stream latency 84 (-7.7%) 113 🔴 (-27%) 💚 133 🔴 (-23%) 💚 213 🔴 (-62%) 💚 30
SO stream overhead (text) 104 (-21%) 💚 196 (-29%) 💚 241 (-27%) 💚 333 (-58%) 💚 30
SO stream overhead (structured) 112 (-8.2%) 179 (-27%) 💚 206 (-34%) 💚 725 (+61%) 🔻 30

2346afa

Mon, 10 Aug 2026 23:45:52 GMT · run logs

vercel / nextjs-turbopack

Metric Scenario Best (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS step 215 (-74%) 💚 1416 🔴 (+28%) 🔻 1460 🔴 (+28%) 🔻 1586 🔴 (+35%) 🔻 30
TTFS stream 225 (+8.7%) 1412 🔴 (+28%) 🔻 1501 🔴 (+35%) 🔻 1845 🔴 (+60%) 🔻 30
TTFS hook + stream 351 (-72%) 💚 1626 🔴 (+18%) 🔻 1727 🔴 (+21%) 🔻 1803 🔴 (+17%) 🔻 30
STSO 1020 steps (inline) 100 (-1.0%) 157 (+6.1%) 183 (+7.6%) 295 (-1.7%) 1019
WO 1020 steps 151943 (+7.2%) 151943 (+7.2%) 151943 (+7.2%) 151943 (+7.2%) 1
SL stream latency 105 (+21%) 🔻 165 🔴 (+46%) 🔻 203 🔴 (+62%) 🔻 929 🔴 (+554%) 🔻 30
SO stream overhead (text) 122 (+11%) 233 (+24%) 🔻 502 🔴 (+137%) 🔻 1068 🔴 (+347%) 🔻 30
SO stream overhead (structured) 117 (+5.4%) 225 (+39%) 🔻 304 (+64%) 🔻 745 (+203%) 🔻 30
ℹ️ Metric definitions & methodology

The collapsed STSO distribution section above buckets every step gap of the sequential-steps run (not a sampled window), split by whether the step ending the gap ran inline — in the same warm process as the step before it, so the gap is pure framework overhead — or after a queue-hop — the first step of a fresh process, which pays queue dispatch, client reinit and event-log replay. Bars overlay the two runs: is main, marks where this run lands, bridges the gap when this run has more samples in a bucket.

Best/P75/P90/P99 deltas compare against the most recent benchmark run on main at the time of this run. 🔻 flags a delta worse than +15%, 💚 one better than −15%.

Metrics — TTFS: time to first step body (in-deployment start() → first step body, deployment clocks) · STSO: step-to-step overhead (gap between consecutive step bodies) · WO: workflow overhead (whole-run time outside step bodies, in-deployment anchored) · SL: stream latency (in-deployment write → read propagation, readAt - writtenAt) · SO: stream overhead (end-to-end write+consume time beyond the modelled generation window)

Scenarios — step: one trivial no-op step, no stream; no hooks, so the run stays in turbo mode (in-process fast path) · stream: one streaming step; no hooks, so the run stays in turbo mode (in-process fast path) · hook + stream: registers a hook before one step, which exits turbo mode (dispatch path) · 1020 steps: 1020 trivial sequential steps; STSO is measured between consecutive steps in the given step ranges, and WO is the whole-run overhead outside step bodies · stream latency: parallel reader/writer steps on a dedicated stream; SL is the in-deployment write->read propagation (readAt - writtenAt) · stream overhead (text): writer streams 300 variable-length text token deltas paced at 100/s for 3s (a haiku-size LLM's token throughput) while a parallel reader drains the whole stream; SO is the end-to-end write+consume time beyond the 3s generation window (overhead/backpressure) · stream overhead (structured): same workload as stream overhead (text), but each delta is an AI-SDK-style structured object ({ type: 'text-delta', id, text }) instead of a raw string, so the SO gap vs the text scenario is the added serialization cost

🔴 marks a percentile over its target (within target is left unmarked). Targets (p75/p90/p99, ms) — TTFS 200/300/600 · SL 50/60/125 · SO 250/500/1000

All metrics are measured from deployment-side timestamps only. Runs are triggered by an in-deployment route that stamps the anchor (clientStart) right before start(), so the CI runner’s request and its path through api.vercel.com sit outside every measured window. TTFS = in-deployment start() → first step body (turbo uses the in-process fast path, non-turbo the dispatch path), and includes the VQS dispatch hop plus any /flow cold start. STSO/WO are measured between step bodies on the deployment. SL is measured inside the workflow (parallel reader/writer steps), so it no longer includes the api.vercel.com read path.

Cold starts are kept in the numbers on purpose — they are part of real bursty-workload latency. The workbench deployment cold-starts the /flow invocation for a large fraction of runs, inflating P75+; the Best column shows the fastest (warm-start) sample for comparison.

@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Sim World

Simulated world deterministic testing for races. Traces

🟠 Mint-ordered log — 6 fail of 41 total

log=mint-ordered · fence=per-spec

scenario outcome events virt replay violations
smoke-no-steps completed 3 0ms ok 0
smoke-one-step completed 6 0ms ok 0
hook-at-step-started completed 12 0ms ok 0
hook-at-step-completed completed 12 0ms ok 0
hook-at-hook-created completed 12 0ms ok 0
deadline-hook-wins completed 7 1.0h ok 0
deadline-expires completed 7 1.0h ok 0
long-sleep completed 11 30.0d ok 0
hook-never-arrives stalled 3 0ms skipped 0
step-retries-twice completed 10 2.0s ok 0
parallel-steps completed 9 0ms ok 0
hook-on-execution-state completed 12 0ms ok 0
peek-hook-before-branch completed 12 0ms ok 0
peek-hook-after-branch completed 12 0ms ok 0
peek-hook-at-registration completed 12 0ms ok 0
race-hook-before-probe completed 12 0ms ok 0
race-hook-after-probe completed 12 0ms ok 0
race-duplicate-delivery completed 13 0ms ok 0
attr-hook-before-step completed 11 0ms ok 0
attr-hook-after-step completed 11 0ms ok 0
attr-from-step-body completed 13 0ms ok 0
fork-hook-after-timeout completed 14 1.0m ok 0
fork-hook-before-timeout completed 14 1.0m ok 0
count-hook-after-timeout completed 17 1.0m ok 0
count-hook-before-timeout completed 20 1.0m ok 0
stale-read-step-count-fork completed 17 1.0m MISMATCH 1
stale-read-equal-step-counts completed 14 1.0m MISMATCH 1
step-vs-step-fork completed 12 0ms MISMATCH 1
step-vs-step-fork-fenced completed 12 0ms MISMATCH 1
fence-catches-benign-direction completed 12 5ms ok 0
in-flight-before-decision completed 17 1.0m MISMATCH 1
in-flight-before-decision-counted completed 20 1.0m ok 0
in-flight-after-decision failed 14 2.0m MISMATCH 1
stale-read-step-count-fork-fenced completed 20 1.0m ok 0
fork-hook-wins completed 13 1.0m ok 0
fork-timeout-wins completed 13 1.0m ok 0
unclaimed-payload-under-fork completed 17 1.0m ok 0
claimed-payload-under-fork completed 17 1.0m ok 0
writers-independent-step-bodies completed 12 0ms ok 0
writers-scripted-tempo completed 12 0ms ok 0
cancel-mid-step cancelled 7 0ms skipped 0

Full trace: world-sim-mint.txt

🟢 Append-only log — 0 fail of 41 total

log=append-only · fence=per-spec

scenario outcome events virt replay violations
smoke-no-steps completed 3 0ms ok 0
smoke-one-step completed 6 0ms ok 0
hook-at-step-started completed 12 0ms ok 0
hook-at-step-completed completed 12 0ms ok 0
hook-at-hook-created completed 12 0ms ok 0
deadline-hook-wins completed 7 1.0h ok 0
deadline-expires completed 7 1.0h ok 0
long-sleep completed 11 30.0d ok 0
hook-never-arrives stalled 3 0ms skipped 0
step-retries-twice completed 10 2.0s ok 0
parallel-steps completed 9 0ms ok 0
hook-on-execution-state completed 12 0ms ok 0
peek-hook-before-branch completed 12 0ms ok 0
peek-hook-after-branch completed 12 0ms ok 0
peek-hook-at-registration completed 12 0ms ok 0
race-hook-before-probe completed 12 0ms ok 0
race-hook-after-probe completed 12 0ms ok 0
race-duplicate-delivery completed 13 0ms ok 0
attr-hook-before-step completed 11 0ms ok 0
attr-hook-after-step completed 11 0ms ok 0
attr-from-step-body completed 13 0ms ok 0
fork-hook-after-timeout completed 14 1.0m ok 0
fork-hook-before-timeout completed 14 1.0m ok 0
count-hook-after-timeout completed 17 1.0m ok 0
count-hook-before-timeout completed 20 1.0m ok 0
stale-read-step-count-fork completed 20 1.0m ok 0
stale-read-equal-step-counts completed 14 1.0m ok 0
step-vs-step-fork completed 12 0ms ok 0
step-vs-step-fork-fenced completed 12 0ms ok 0
fence-catches-benign-direction completed 12 5ms ok 0
in-flight-before-decision completed 17 1.0m ok 0
in-flight-before-decision-counted completed 17 1.0m ok 0
in-flight-after-decision completed 19 2.0m ok 0
stale-read-step-count-fork-fenced completed 20 1.0m ok 0
fork-hook-wins completed 13 1.0m ok 0
fork-timeout-wins completed 13 1.0m ok 0
unclaimed-payload-under-fork completed 17 1.0m ok 0
claimed-payload-under-fork completed 17 1.0m ok 0
writers-independent-step-bodies completed 12 0ms ok 0
writers-scripted-tempo completed 12 0ms ok 0
cancel-mid-step cancelled 7 0ms skipped 0

Full trace: world-sim-append-only.txt

@TooTallNate TooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at cc1a14c (based on current main, 0 behind). Full core suite green locally (93 files, 2105 passed / 3 expected fail), including the 66 TTR-specific tests.

Verified — the properties the design claims:

  • The phase sum telescopes exactly: total = T7−T0 and the phases are consecutive differences, so sum-equals-total holds by construction — including the optimistic-start case where T6 doesn't exist and step_prepare spans T5→T7 rather than inventing a claim-completion instant. Dropping a non-monotonic set instead of clamping is the right call for a decomposition whose whole value is that the phases are trustworthy; the one skew direction that can't be detected (consumer clock ahead inflating queue_delivery) is inherent to any cross-machine measurement.
  • The re-route attribution is exact, not approximate — this was my main verification target given the deployment-affinity work. All three forwarding sites carry hookResumeTiming verbatim with the right phase in mind: the misrouted-flow re-routes forward it unmodified (no T2 stamp) so the extra hop lands in queue_delivery and T2 is the final consumer's entry; the step re-route keeps its hop inside step_dispatch. The comments at each site say exactly why.
  • One resumption, one sample: the reported latch is shared across an inline batch and set only on successful emission, so the sample isn't pinned to a step that loses its create-claim, retries are excluded (attempt !== 1), and a resumption whose boundaries never validate emits nothing rather than something wrong. The dispatched-step handoff (resumeTimingForMessage → message → resumeTrackingFromMessage) round-trips verbatim, with a test pinning it.
  • The schema has internalized the capabilities-schema lesson: .catch(undefined) on the payload field with the burn-the-delivery-budget rationale written down, strategy/setupSource as plain strings so a future producer's value can't fail the invocation parse, and the three-direction rolling-deploy compatibility stated explicitly. The NaN placeholder for a producer-only message is delicate but fail-closed — validateBoundaries rejects it if the overwrite ever doesn't happen, and there's a test for exactly that.
  • T0 opens at both public entry points (API and webhook handler), with the comment explaining why the webhook path must include its by-token lookup — otherwise webhook resumes would systematically under-report into the same metric. The event_load honest-default dimension being documented as not-currently-produced rather than silently dead is a nice touch.

One ask: the @workflow/world changeset should be minor, not patchHookResumeTimingSchema/HookResumeTiming are new exported API and hookResumeTiming is new wire surface, same class as the deploymentId field which shipped as minor.

CI: only the ubiquitous python-workbench baseline deploy failure. Test coverage hits every edge I went looking for (sum exactness, claim omission, inversion drop, zero-length phases, old-producer/old-consumer compat).

This closes the observability gap between resumeHook() and the ttfs/rsfs work — the resume path was the last leg without a decomposition. Approving.

Report end-to-end TTR for a hook resumption — entry into the public resume
API through to the first line of the next durable step — on that step's
`step.execute` span, decomposed into non-overlapping phases that sum
exactly to the total:

  workflow.resume.total_ms
  workflow.resume.phase.{producer_prep,queue_delivery,resume_setup,
                         replay,step_dispatch,step_claim,step_prepare}_ms

dimensioned by trigger, dispatch strategy, setup source, and whether the
step ran inline or was dispatched to another invocation.

T0 is stamped by whichever public entry point the caller used, so
`resumeWebhook` — which does its own by-token lookup and key resolution
before reaching the shared implementation — measures the same window as
`resumeHook` rather than a systematically shorter one. T7 is taken inside
`contextStorage.run`, immediately before `stepFn.apply()`, so the
`step_prepare` phase covers the step-context setup it is defined to cover.

`resumeHook()` puts the producer boundaries on an optional
`hookResumeTiming` field on the queue message (both dispatch paths); the
consuming invocation adds its own and hands them to the execution that
will actually ATTEMPT the next durable step. That decision is made against
the dispatch loop's own classification, so an owned-recovery step keeps
the measurement here instead of it riding off on a queued sibling, and a
step converted into a delayed backstop wake — which this delivery does not
attempt — never takes it. Within an inline batch the tracking is shared
and a one-shot latch picks the single step that reaches user code, so the
sample survives the batch's first step losing its create-claim. A
deployment-affinity re-route forwards the timing untouched, keeping the
wasted hop inside `queue_delivery`.

The field is optional in every direction (new producer/old consumer, new
consumer/old message, no workflow-server change) and parses with
`.catch(undefined)` so a malformed value can never fail a delivery. A
sample is emitted only when every required boundary is present, finite,
and monotonic — a skewed or incomplete set is dropped rather than
reported as a negative phase.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for a0ccfe0 (AI decision).

This is feature work: it adds a new OTEL telemetry surface (workflow.resume.total_ms plus seven phase attributes and four dimensions), a new resume-latency.ts module, and a new optional hookResumeTiming field on the queue message payload. Nothing here fixes a defect, crash, or correctness problem on stable — it adds observability capability, which belongs on main only.

To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

a0ccfe0f50df1e6726b033e91c41257065e20edd

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants