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

@karthikscale3karthikscale3 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 ownerAugust 10, 2026 23:26
@changeset-bot

changeset-botBot 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
NameType
@workflow/corePatch
@workflow/worldPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/world-localPatch
@workflow/world-postgresPatch
@workflow/world-vercelPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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

vercelBot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

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

ProjectDeploymentActionsUpdated (UTC)
example-nextjs-workflow-turbopackReadyReadyPreviewAug 12, 2026 6:03pm
example-nextjs-workflow-webpackReadyReadyPreviewAug 12, 2026 6:03pm
example-workflowReadyReadyPreviewAug 12, 2026 6:03pm
workbench-astro-workflowReadyReadyPreviewAug 12, 2026 6:03pm
workbench-express-workflowReadyReadyPreviewAug 12, 2026 6:03pm
workbench-fastify-workflowReadyReadyPreviewAug 12, 2026 6:03pm
workbench-hono-workflowReadyReadyPreviewAug 12, 2026 6:03pm
workbench-nestjs-workflowReadyReadyPreviewAug 12, 2026 6:03pm
workbench-nitro-workflowReadyReadyPreviewAug 12, 2026 6:03pm
workbench-nuxt-workflowReadyReadyPreviewAug 12, 2026 6:03pm
workbench-python-workflowErrorErrorAug 12, 2026 6:03pm
workbench-sveltekit-workflowReadyReadyPreviewAug 12, 2026 6:03pm
workbench-tanstack-start-workflowReadyReadyPreviewAug 12, 2026 6:03pm
workbench-vite-workflowReadyReadyPreviewAug 12, 2026 6:03pm
workflow-docsReadyReadyPreview, v0Aug 12, 2026 6:03pm
workflow-swc-playgroundReadyReadyPreviewAug 12, 2026 6:03pm
workflow-tarballsReadyReadyPreviewAug 12, 2026 6:03pm
workflow-webReadyReadyPreviewAug 12, 2026 6:03pm

@github-actions

github-actionsBot 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
PassedFailedSkippedTotal
✅ ▲ Vercel Production346605904056
✅ 💻 Local Development381005584368
✅ 📦 Local Production381005584368
❌ 🐘 Local Postgres380915584368
✅ 🪟 Windows15600156
✅ vercel-multi-region270027
Total150781226417343
Details by Category

✅ ▲ Vercel Production

AppPassedFailedSkipped
✅ astro-node128028
✅ astro-quickjs128028
✅ example-node128028
✅ example-quickjs128028
✅ express-node128028
✅ express-quickjs128028
✅ fastify-node128028
✅ fastify-quickjs128028
✅ hono-node128028
✅ hono-quickjs128028
✅ nest-node128028
✅ nest-quickjs128028
✅ nextjs-turbopack-node15303
✅ nextjs-turbopack-quickjs15303
✅ nextjs-webpack-node15303
✅ nextjs-webpack-quickjs15303
✅ nitro-node128028
✅ nitro-quickjs128028
✅ nuxt-node128028
✅ nuxt-quickjs128028
✅ sveltekit-node14709
✅ sveltekit-quickjs14709
✅ tanstack-start-node128028
✅ tanstack-start-quickjs128028
✅ vite-node128028
✅ vite-quickjs128028

✅ 💻 Local Development

AppPassedFailedSkipped
✅ astro-stable-node130026
✅ astro-stable-quickjs130026
✅ express-stable-node130026
✅ express-stable-quickjs130026
✅ fastify-stable-node130026
✅ fastify-stable-quickjs130026
✅ hono-stable-node130026
✅ hono-stable-quickjs130026
✅ nest-stable-node130026
✅ nest-stable-quickjs130026
✅ nextjs-turbopack-canary-node137019
✅ nextjs-turbopack-canary-quickjs137019
✅ nextjs-turbopack-stable-node15600
✅ nextjs-turbopack-stable-quickjs15600
✅ nextjs-webpack-canary-node137019
✅ nextjs-webpack-canary-quickjs137019
✅ nextjs-webpack-stable-node15600
✅ nextjs-webpack-stable-quickjs15600
✅ nitro-stable-node130026
✅ nitro-stable-quickjs130026
✅ nuxt-stable-node130026
✅ nuxt-stable-quickjs130026
✅ sveltekit-stable-node14907
✅ sveltekit-stable-quickjs14907
✅ tanstack-start-node130026
✅ tanstack-start-quickjs130026
✅ vite-stable-node130026
✅ vite-stable-quickjs130026

✅ 📦 Local Production

AppPassedFailedSkipped
✅ astro-stable-node130026
✅ astro-stable-quickjs130026
✅ express-stable-node130026
✅ express-stable-quickjs130026
✅ fastify-stable-node130026
✅ fastify-stable-quickjs130026
✅ hono-stable-node130026
✅ hono-stable-quickjs130026
✅ nest-stable-node130026
✅ nest-stable-quickjs130026
✅ nextjs-turbopack-canary-node137019
✅ nextjs-turbopack-canary-quickjs137019
✅ nextjs-turbopack-stable-node15600
✅ nextjs-turbopack-stable-quickjs15600
✅ nextjs-webpack-canary-node137019
✅ nextjs-webpack-canary-quickjs137019
✅ nextjs-webpack-stable-node15600
✅ nextjs-webpack-stable-quickjs15600
✅ nitro-stable-node130026
✅ nitro-stable-quickjs130026
✅ nuxt-stable-node130026
✅ nuxt-stable-quickjs130026
✅ sveltekit-stable-node14907
✅ sveltekit-stable-quickjs14907
✅ tanstack-start-node130026
✅ tanstack-start-quickjs130026
✅ vite-stable-node130026
✅ vite-stable-quickjs130026

❌ 🐘 Local Postgres

AppPassedFailedSkipped
✅ astro-stable-node130026
✅ astro-stable-quickjs130026
✅ express-stable-node130026
✅ express-stable-quickjs130026
✅ fastify-stable-node130026
✅ fastify-stable-quickjs130026
✅ hono-stable-node130026
✅ hono-stable-quickjs130026
✅ nest-stable-node130026
✅ nest-stable-quickjs130026
✅ nextjs-turbopack-canary-node137019
✅ nextjs-turbopack-canary-quickjs137019
✅ nextjs-turbopack-stable-node15600
✅ nextjs-turbopack-stable-quickjs15600
✅ nextjs-webpack-canary-node137019
✅ nextjs-webpack-canary-quickjs137019
✅ nextjs-webpack-stable-node15600
❌ nextjs-webpack-stable-quickjs15510
✅ nitro-stable-node130026
✅ nitro-stable-quickjs130026
✅ nuxt-stable-node130026
✅ nuxt-stable-quickjs130026
✅ sveltekit-stable-node14907
✅ sveltekit-stable-quickjs14907
✅ tanstack-start-node130026
✅ tanstack-start-quickjs130026
✅ vite-stable-node130026
✅ vite-stable-quickjs130026

✅ 🪟 Windows

AppPassedFailedSkipped
✅ nextjs-turbopack-quickjs15600

✅ vercel-multi-region

AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

@github-actions

github-actionsBot 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

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep1366 (+34%) 🔻1479 🔴 (+37%) 🔻1507 🔴 (+33%) 🔻1632 🔴 (±0%)30
TTFSstream342 (+32%) 🔻1477 🔴 (+39%) 🔻1519 🔴 (+39%) 🔻1594 🔴 (+30%) 🔻30
TTFShook + stream359 (-19%) 💚1798 🔴 (+30%) 🔻1864 🔴 (+30%) 🔻2017 🔴 (+26%) 🔻30
STSO1020 steps (inline)131 (+20%) 🔻177 (-4.3%)197 (-8.4%)330 (-16%) 💚1019
WO1020 steps177797 (-1.6%)177797 (-1.6%)177797 (-1.6%)177797 (-1.6%)1
SLstream latency107 (+15%) 🔻130 🔴 (-12%)170 🔴 (+10%)261 🔴 (+20%) 🔻30
SOstream overhead (text)119 (+10%)211 (-7.9%)328 (+14%)431 (-55%) 💚30
SOstream 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

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep270 (-69%) 💚1470 🔴 (+20%) 🔻1481 🔴 (+13%)1619 🔴 (-10%)30
TTFSstream232 (-78%) 💚1348 🔴 (+19%) 🔻1458 🔴 (+25%) 🔻1476 🔴 (+19%) 🔻30
TTFShook + stream1550 (+10%)1715 🔴 (+10%)1776 🔴 (+12%)5960 🔴 (+273%) 🔻30
STSO1020 steps (inline)121 (-23%) 💚159 (-36%) 💚177 (-43%) 💚277 (-61%) 💚1019
WO1020 steps159780 (-34%) 💚159780 (-34%) 💚159780 (-34%) 💚159780 (-34%) 💚1
SLstream latency83 (-34%) 💚115 🔴 (-38%) 💚134 🔴 (-43%) 💚183 🔴 (-42%) 💚30
SOstream overhead (text)105 (-29%) 💚176 (-29%) 💚212 (-39%) 💚3418 🔴 (+475%) 🔻30
SOstream 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

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep1285 (+22%) 🔻1349 🔴 (+15%)1381 🔴 (+15%)1403 🔴 (-18%) 💚30
TTFSstream241 (-77%) 💚1366 🔴 (+20%) 🔻1397 🔴 (+20%) 🔻1453 🔴 (+20%) 🔻30
TTFShook + stream1551 (+24%) 🔻1649 🔴 (+21%) 🔻1758 🔴 (+26%) 🔻1819 🔴 (+21%) 🔻30
STSO1020 steps (inline)102 (+6.3%)136 (-17%) 💚158 (-22%) 💚281 (-64%) 💚1019
WO1020 steps137368 (-20%) 💚137368 (-20%) 💚137368 (-20%) 💚137368 (-20%) 💚1
SLstream latency84 (-7.7%)113 🔴 (-27%) 💚133 🔴 (-23%) 💚213 🔴 (-62%) 💚30
SOstream overhead (text)104 (-21%) 💚196 (-29%) 💚241 (-27%) 💚333 (-58%) 💚30
SOstream 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

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep215 (-74%) 💚1416 🔴 (+28%) 🔻1460 🔴 (+28%) 🔻1586 🔴 (+35%) 🔻30
TTFSstream225 (+8.7%)1412 🔴 (+28%) 🔻1501 🔴 (+35%) 🔻1845 🔴 (+60%) 🔻30
TTFShook + stream351 (-72%) 💚1626 🔴 (+18%) 🔻1727 🔴 (+21%) 🔻1803 🔴 (+17%) 🔻30
STSO1020 steps (inline)100 (-1.0%)157 (+6.1%)183 (+7.6%)295 (-1.7%)1019
WO1020 steps151943 (+7.2%)151943 (+7.2%)151943 (+7.2%)151943 (+7.2%)1
SLstream latency105 (+21%) 🔻165 🔴 (+46%) 🔻203 🔴 (+62%) 🔻929 🔴 (+554%) 🔻30
SOstream overhead (text)122 (+11%)233 (+24%) 🔻502 🔴 (+137%) 🔻1068 🔴 (+347%) 🔻30
SOstream 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-actionsBot 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

scenariooutcomeeventsvirtreplayviolations
smoke-no-stepscompleted30msok0
smoke-one-stepcompleted60msok0
hook-at-step-startedcompleted120msok0
hook-at-step-completedcompleted120msok0
hook-at-hook-createdcompleted120msok0
deadline-hook-winscompleted71.0hok0
deadline-expirescompleted71.0hok0
long-sleepcompleted1130.0dok0
hook-never-arrivesstalled30msskipped0
step-retries-twicecompleted102.0sok0
parallel-stepscompleted90msok0
hook-on-execution-statecompleted120msok0
peek-hook-before-branchcompleted120msok0
peek-hook-after-branchcompleted120msok0
peek-hook-at-registrationcompleted120msok0
race-hook-before-probecompleted120msok0
race-hook-after-probecompleted120msok0
race-duplicate-deliverycompleted130msok0
attr-hook-before-stepcompleted110msok0
attr-hook-after-stepcompleted110msok0
attr-from-step-bodycompleted130msok0
fork-hook-after-timeoutcompleted141.0mok0
fork-hook-before-timeoutcompleted141.0mok0
count-hook-after-timeoutcompleted171.0mok0
count-hook-before-timeoutcompleted201.0mok0
stale-read-step-count-forkcompleted171.0mMISMATCH1
stale-read-equal-step-countscompleted141.0mMISMATCH1
step-vs-step-forkcompleted120msMISMATCH1
step-vs-step-fork-fencedcompleted120msMISMATCH1
fence-catches-benign-directioncompleted125msok0
in-flight-before-decisioncompleted171.0mMISMATCH1
in-flight-before-decision-countedcompleted201.0mok0
in-flight-after-decisionfailed142.0mMISMATCH1
stale-read-step-count-fork-fencedcompleted201.0mok0
fork-hook-winscompleted131.0mok0
fork-timeout-winscompleted131.0mok0
unclaimed-payload-under-forkcompleted171.0mok0
claimed-payload-under-forkcompleted171.0mok0
writers-independent-step-bodiescompleted120msok0
writers-scripted-tempocompleted120msok0
cancel-mid-stepcancelled70msskipped0

Full trace: world-sim-mint.txt

🟢 Append-only log — 0 fail of 41 total

log=append-only · fence=per-spec

scenariooutcomeeventsvirtreplayviolations
smoke-no-stepscompleted30msok0
smoke-one-stepcompleted60msok0
hook-at-step-startedcompleted120msok0
hook-at-step-completedcompleted120msok0
hook-at-hook-createdcompleted120msok0
deadline-hook-winscompleted71.0hok0
deadline-expirescompleted71.0hok0
long-sleepcompleted1130.0dok0
hook-never-arrivesstalled30msskipped0
step-retries-twicecompleted102.0sok0
parallel-stepscompleted90msok0
hook-on-execution-statecompleted120msok0
peek-hook-before-branchcompleted120msok0
peek-hook-after-branchcompleted120msok0
peek-hook-at-registrationcompleted120msok0
race-hook-before-probecompleted120msok0
race-hook-after-probecompleted120msok0
race-duplicate-deliverycompleted130msok0
attr-hook-before-stepcompleted110msok0
attr-hook-after-stepcompleted110msok0
attr-from-step-bodycompleted130msok0
fork-hook-after-timeoutcompleted141.0mok0
fork-hook-before-timeoutcompleted141.0mok0
count-hook-after-timeoutcompleted171.0mok0
count-hook-before-timeoutcompleted201.0mok0
stale-read-step-count-forkcompleted201.0mok0
stale-read-equal-step-countscompleted141.0mok0
step-vs-step-forkcompleted120msok0
step-vs-step-fork-fencedcompleted120msok0
fence-catches-benign-directioncompleted125msok0
in-flight-before-decisioncompleted171.0mok0
in-flight-before-decision-countedcompleted171.0mok0
in-flight-after-decisioncompleted192.0mok0
stale-read-step-count-fork-fencedcompleted201.0mok0
fork-hook-winscompleted131.0mok0
fork-timeout-winscompleted131.0mok0
unclaimed-payload-under-forkcompleted171.0mok0
claimed-payload-under-forkcompleted171.0mok0
writers-independent-step-bodiescompleted120msok0
writers-scripted-tempocompleted120msok0
cancel-mid-stepcancelled70msskipped0

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

@TooTallNateTooTallNate 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 freeto 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

@karthikscale3@TooTallNate