feat(producer): calibration-aware heartbeat + worker-death terminal-error contract#2510
Conversation
…rror contract Field signals ts=1784019503 (heartbeat reports 0 frames during 64s browser calibration — reads as broken but is healthy) and ts=1784042064 (1292s Windows render hard-exited during video frame extraction with no final error string — silent worker crash). Add calibrating/capturing state to heartbeat labels; surface synthetic terminal error on unexpected worker exit when no explicit error was emitted. Stack: PR #6 of 9 (base via/overlay-count-lint). Signed-off-by: Via <vance@heygen.com>
miga-heygen
left a comment
There was a problem hiding this comment.
Review — #2510 feat(producer): calibration-aware heartbeat + worker-death terminal-error contract
Verdict: LGTM — two clean fixes for two distinct field signals, well-tested.
SSOT check
1. Silent worker exit
| Item | Owner |
|---|---|
| Expected-frame count | expectedFramesForTask — single function, honors stride |
| Synthesized error message | synthesizeSilentWorkerExitError — single function |
| Error synthesis call site | executeParallelCapture — single in-place mutation before the error filter |
formatWorkerFailure now falls through to synthesizeSilentWorkerExitError when result.error is falsy or empty. The in-place mutation in executeParallelCapture ensures the downstream results.filter(r => r.error) catches the synthesized failure. Clean.
expectedFramesForTask correctly handles stride: ceil((endFrame - startFrame) / stride). Contiguous (stride=1), interleaved (stride>1), empty ranges (start >= end → 0) all tested.
2. Calibration heartbeat
| Item | Owner |
|---|---|
| Heartbeat message override | ObserveRenderStageOptions.heartbeatMessage |
| Stage phase discrimination | stagePhase: "calibrating" | "capturing" in observation data |
The heartbeatMessage option fully replaces the default "stage still running" — not appended. Test verifies no "stage still running" heartbeat emits during a calibration stage. The stagePhase field is spread with the default first (stagePhase: "capturing" in captureStageObservationData) so the calibration call site can override via extra.
Tests
- Silent exit:
formatWorkerFailurewith no error (synthesizes message), with explicit error (prefers it), with empty string error (treated as silent).expectedFramesForTask: contiguous, interleaved, stride remainder (ceil), empty ranges.synthesizeSilentWorkerExitError: field signal reference, frame shortfall details, reruns hint. - Heartbeat: Custom message during calibration (no "stage still running" leak), default message without override.
No blockers. Ship it.
Field-signal baseline: >=2 fallbacks/hr on darwin/arm64 from filter:blur and filter:drop-shadow triggers. Fallback path perf is currently untimed, so we can't know if the overhead is 10% or 10x. This PR adds opt-in per-frame timing (HF_PROFILE_FALLBACK_CAPTURE=true) that emits p50/p95/p99 + trigger reason via the observeRenderStage telemetry channel extended in #2510. Diagnostic surface only -- no perf fix, no behavior change on healthy paths. Stack: PR #9 (final) of 9 (base via/escape-hatch-fallback-reproducer). Signed-off-by: Via
Description
Two additive parallel-capture observability improvements that make healthy runs distinguishable from broken ones in the render trace:
Fix 1 — Calibration-aware heartbeat label.
observeRenderStagenow accepts an optionalheartbeatMessageoverride, and thebrowser_probe+capture_calibrationcall sites pass"browser calibrating (frames not started)"so their heartbeats no longer share the"stage still running"phrasing with post-capture stages. A newstagePhase: "calibrating" | "capturing"field flows through the observation data (added toALLOWED_STRING_DATA_KEYS, defaulted insidecaptureStageObservationData, overridden at the calibration call site) so structured consumers can classify the heartbeat without string-matching the message.Fix 2 — Worker-death terminal-error contract.
formatWorkerFailurenow synthesizes a terminal error string whenresult.erroris falsy (undefined or empty), naming the framesCaptured shortfall, the expected count, the range, the field-signal reference, and the--workers=1isolation hint.executeParallelCapturenow also detects under-captured but no-error results — a worker returning success withframesCaptured < expectedgets an error string written toresult.errorin place, so the downstreamresults.filter((r) => r.error)doesn't silently accept it. Two exports added to support both branches:expectedFramesForTask(task)(stride-aware) andsynthesizeSilentWorkerExitError(result, expected).Both changes are additive — the existing "stage still running" message is preserved everywhere else, and workers that already surface a bare error still get their explicit message untouched (the synthesized string only fills in when
result.erroris falsy).Context
Two field signals define the observability failure surface. Both concern healthy or broken runs that the current instrumentation cannot tell apart:
ts=1784019503(#hyperframes-cli-feedback): parallel-capture heartbeat/observability during startup underinstruments healthy runs. On a ~64s browser calibration (SwiftShader warm-up class), the heartbeat emitted at 30s and 60s carriedframesCompleted: 0— read as a broken zero-frame stall — while calibration probe frames were legitimately being written to disk. The stage phase iscapture_calibration(which usescaptureStageObservationData— that helper'sframesCompletedgetter returnsjob.framesRendered ?? 0, and calibration doesn't touchjob.framesRenderedbecause it writes probe frames only). Directly covered here — the calibration call sites now override the heartbeat message and passstagePhase: "calibrating"in the data, so consumers can distinguish healthy pre-capture waits from mid-capture zero-frame stalls.ts=1784042064(#hyperframes-cli-feedback): a 1292s Windows render hard-exited during video frame extraction afterheartbeatIndex: 3(~180s) with no final error string. The parallel-worker path inparallelCoordinator.tstreats aWorkerResultwitherror === undefinedas success and never surfaces the failure — the parent'sexecuteParallelCapturefiltersresults.filter((r) => r.error)so a silent no-error result gets dropped from the aggregated failure message entirely. Directly covered here — under-captured no-error results now get a synthesized error string in place, andformatWorkerFailurealso fills in a synthesized message when it's asked to format a result whose error field is missing or empty. Both paths cite the field signal +--workers=1isolation hint so operators grepping logs can classify the failure.The heartbeat message +
stagePhasefield are backward-compatible additions — no consumer that readsmessage === "stage still running"regresses (the default is unchanged), andRenderObservationDataremains aRecord<string, RenderObservationValue>. The worker-death contract keeps the current happy-path behavior for workers that emit real error strings; the synthesized message only fills gaps where the current code emits"unknown error"(informatWorkerFailure) or silently passes the result through (inexecuteParallelCapture).Testing
packages/producer/src/services/render/observability.test.ts— 11 pass, 0 fail. Two new cases:uses a custom heartbeat message during calibration stages— verifies the custom message replaces"stage still running"(no both-emission), thatstagePhase: "calibrating"andframesCompleted: 0flow through the heartbeat data, and thatheartbeatIndex: 1still increments as expected.keeps the default heartbeat message when no override is passed— verifies"stage still running"is unchanged when the fifth arg is omitted, and thatstagePhase: "capturing"flows through when the caller sets it.packages/engine/src/services/parallelCoordinator.test.ts— 34 pass, 0 fail. Six new cases across three describes:synthesizes a terminal error when a worker exits with no error string— verifies the synthesized message names the worker, the shortfall (12 captured / 60 expected), the range[0, 60), the field signalts=1784042064, and the--workers=1hint.prefers an explicit error string over the synthesized silent-exit message— verifies real errors (e.g.Target closed) are unchanged and the field-signal reference does NOT appear.treats an empty error string as a silent exit for the message contract— closes the "empty-string is falsy but not undefined" gap.expectedFramesForTask— contiguous range, stride-3 interleaved (30 → 10 frames), remainder round-up (31 → 11), empty range → 0, reversed range → 0.synthesizeSilentWorkerExitError— direct-call test verifying every field lands in the message.bun run typecheck— clean on both@hyperframes/producerand@hyperframes/engine.oxfmt --write— clean on all 5 touched files.ffprobe.test.tsfailures (3 cases about HDR PNG cICP metadata) are unrelated to this PR — they touchpackages/engine/src/utils/ffprobe.tsand predatevia/overlay-count-lint.Enterprise release / feature flag holdout
UX/Screenshot recording
Stack: PR #6 of 9. Base:
via/overlay-count-lint(#2508).— Via
Co-Authored-By: Claude Opus 4.7 noreply@anthropic.com