Skip to content

feat(producer,cli): full telemetry visibility for DE parallel-router/inversion failures#2115

Merged
vanceingalls merged 13 commits into
mainfrom
de-parallel-router-failure-telemetry
Jul 11, 2026
Merged

feat(producer,cli): full telemetry visibility for DE parallel-router/inversion failures#2115
vanceingalls merged 13 commits into
mainfrom
de-parallel-router-failure-telemetry

Conversation

@vanceingalls

@vanceingalls vanceingalls commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Summary

Follow-up to #2095. Started as failure-path telemetry visibility for the DE parallel router, then grew into hardening the retry/fallback path itself as testing (including live dogfooding) turned up real gaps — including three defects caught by a max-effort code review of the branch — and finally into solving how the router ever gets real-traffic telemetry at all, since HF_DE_PARALLEL_ROUTER has no self-serve opt-in path.

1. Failure-path telemetry visibility. render_error carried zero DE-cohort context, so a hard failure while routed/inverted was indistinguishable from any unrelated failure in PostHog.

  • RenderCaptureObservability now records dePreInversionWorkers/dePreRouterWorkers (the worker count calibration would have picked absent the experiment), deFallbackReason, mirrored alongside the existing deSelfVerifyFallback — set live at the same point the router/inversion state is set, so it survives into job.errorDetails on a hard failure, not just into the success-path perfSummary.
  • Added capture-sourced de_* fields to the shared RenderObservabilityTelemetryPayload, consumed by both trackRenderComplete and trackRenderError. On render_complete, the existing explicit perfSummary-sourced fields still win — this addition is purely a failure-path fallback.

2. Widen the self-verify retry to generic capture failures on a pinned worker count. The router/inversion pin a fixed worker count regardless of calibration or host load — exactly the scenario a contention timeout or worker crash is most likely under. Previously only a drawElement self-verify failure (blank/PSNR) triggered the existing fallback to the calibrated, non-DE parallel-screenshot path; any other capture-stage failure on a pinned render just hard-failed instead of reusing that tested safety net. shouldRetryViaPinnedFallback (new, pure, unit-tested) widens the retry.

3. Drop to a single worker specifically on OOM. Reusing the pre-pinned worker count on retry is fine for ordinary failures, but calibration's own pick can be higher than the pin (e.g. calibration wanted 5, the router pinned to 3) — retrying an OOM at the same or higher count just OOMs again. resolveInversionRetryPlan/resolveParallelRouterRetryPlan now drop to workerCount=1 specifically when the retry is OOM-triggered.

4. Recognize Bun/JavaScriptCore's OOM message. The producer's deployed runtime is Bun, not Node — isMemoryExhaustionError's patterns were all V8-specific, so #3 would never have engaged in production. Fixed with an exact-match check for Bun's "Out of memory" string.

5. Code-review fixes (3 confirmed findings from a 17-agent max-effort review):

  • The Bun OOM regex from feat(studio): consolidate into single OSS-ready NLE editor #4 was itself defeated by this codebase's own parallel-worker error wrapping ("[Parallel] Capture failed: Worker N: Out of memory") — added a second pattern that survives the wrapping.
  • shouldRetryViaPinnedFallback didn't exclude cancellation — aborting mid-render on the pinned cohort would detour through a wasted encoder spin-up before the cancellation was honored. Added an isCancellation check, checked first.
  • deFallbackReason was set locally but never reached failure-path telemetry — a render that fails after a fallback attempt was indistinguishable from one that never attempted a fallback. Threaded through the same path as deSelfVerifyFallback.

6. CLI trial to actually get real-traffic router telemetry. HF_DE_PARALLEL_ROUTER is a producer env var with no self-serve path — nobody was ever going to manually turn it on, so the soak this whole feature needs would never start. renderLocal now enables the router for free on EVERY eligible CLI render for a fresh install — not just once — to maximize successful ("routed") telemetry volume across the fleet. It only turns off, permanently for that install, the first time the router's OWN safety net actually fires (deParallelRouter === "reverted") — a clean "routed" success keeps the trial running on the next render. A render that merely "routed" then crashed for an unrelated reason (e.g. cancellation) does NOT count as a router failure. Persists to ~/.hyperframes/config.json. A render that never crosses the router's own frame threshold doesn't touch the trial state either way. Never overrides an explicit user-set env var, and only engages when telemetry is enabled. Scoped to the in-process CLI path (--docker doesn't thread perfSummary/errorDetails back to the CLI process, so trial consumption can't be detected there).

Failure modes this retry/fallback path now handles

# Failure mode Trigger Remedy
1 DE self-verify blank frame Drain-time blank-guard trips Retry via screenshot at pre-pinned worker count, deFallbackReason="blank"
2 DE self-verify PSNR breach Captured frame vs ground truth < HF_DE_VERIFY_MIN_DB Same, deFallbackReason="psnr"
3 Generic capture failure on a pinned worker count Host contention timeout, worker crash while inverted/routed Retry at pre-pinned (calibrated) worker count, deFallbackReason="capture_error"
4 OOM on a pinned worker count Memory-exhaustion error while inverted/routed Retry at workerCount=1 (not the pinned count — that can be equal/higher), deFallbackReason="oom"
5 OOM message not recognized (V8-only patterns) Producer runs on Bun/JSC, not Node/V8 isMemoryExhaustionError also matches Bun's exact "Out of memory" message
6 OOM message mangled by this codebase's own worker-error wrapping executeParallelCapture wraps as "[Parallel] Capture failed: Worker N: Out of memory" Second regex matches "out of memory" as the whole content of a "Worker N: ..." segment
7 Cancellation mid-retry User aborts while on the pinned cohort Excluded entirely — propagates immediately instead of spinning up a fresh encoder first
8 True unbounded-growth OOM (heap hard limit) Continuous allocation past the VM ceiling No remedy — not catchable. SIGKILL (exit 137) before any catch runs; documented ceiling, not fixed
9 Retry poisoning a concurrent render (pre-fix) Router mutated process.env.HF_DE_PARALLEL_STREAM globally; renders can share a process (PRODUCER_MAX_CONCURRENT_RENDERS) Router decision threaded as a per-render local (deParallelStreamForced), not an env var
10 Failure invisible in telemetry Hard failure while routed/inverted carried no DE-cohort tags in render_error Mirrored router/inversion/fallback state from live capture-observability into the failure path
11 Router never gets real-traffic telemetry HF_DE_PARALLEL_ROUTER has no self-serve opt-in — nobody manually flips it CLI's per-install trial (item 6) runs on every eligible render until a real failure (not first engagement), maximizing successful-routing telemetry volume; no default-flip or env var needed

Known gaps, not remedied by design:

  • No host-load/contention check before routing — the pin fires regardless of current machine state; failures are only caught reactively (rows 3–6), not avoided proactively.
  • True heap-exhaustion OOM (row 8) is fundamentally uncatchable in this runtime.
  • The CLI trial (row 11) only covers the in-process (--docker-less) CLI path — Docker renders and Studio server renders aren't included.

Test plan

  • bunx vitest run across renderOrchestrator.test.ts, observability.test.ts, events.test.ts, renderObservability.test.ts, frameCapture-transientErrors.test.ts, render.test.ts — 269/269 pass
  • Tests covering: failure-path de_* telemetry on render_error; explicit perfSummary values winning over the capture-observability fallback on render_complete; shouldRetryViaPinnedFallback's verify/generic/OOM/cancellation/reverted-cohort matrix; OOM-drops-to-1 for both retry-plan functions; Bun OOM detection (bare message + wrapped-worker-message forms, including negative substring cases); CLI trial enable/skip/no-consume-on-clean-routed-success/no-consume-on-unrelated-crash/consume-only-on-reverted matrix (40 tests total in render.test.ts)
  • Live dogfood: real HF_DE_PARALLEL_ROUTER=true render confirmed router fires, self-verify passes 4/4, preRouterWorkers populates correctly
  • End-to-end verification (not just unit tests) of the OOM path from inside the workspace: a real Bun RangeError/"Out of memory", both bare and wrapped in the real executeParallelCapture format, correctly resolves to workerCount: 1 on retry
  • End-to-end verification of the CLI trial's config persistence against a real file (not the mocked unit tests): fresh install → deParallelRouterTrialFired undefined → write true → persists across reread
  • tsc --noEmit, oxlint, oxfmt clean on all touched files

🤖 Generated with Claude Code

vanceingalls and others added 2 commits July 9, 2026 15:31
…inversion failures

render_error previously carried zero DE-cohort context — a hard failure while
routed (worker crash, OOM, capture timeout from the fixed 3-worker pin
overriding calibration) was indistinguishable from any other failure. The
data existed (RenderCaptureObservability is mutated live and survives into
job.errorDetails on the failure path) but was never projected into the
render_error payload, which only ever drew de_* fields from perfSummary
(success-only).

- RenderCaptureObservability now also records dePreInversionWorkers /
  dePreRouterWorkers — the worker count calibration would have picked absent
  the experiment — so a resource-pressure failure can be correlated with the
  router overriding a lower calibrated count.
- New capture-sourced de_* fields on RenderObservabilityTelemetryPayload,
  shared by trackRenderComplete and trackRenderError. Explicit
  perfSummary-sourced fields still win on render_complete (spread moved
  first in the event object) — this is purely a failure-path fallback.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…nned worker count

The router/inversion pin a fixed worker count regardless of calibration —
exactly the scenario a host-contention timeout or worker crash is most
likely under. Previously only a drawElement self-verify failure (blank
frame / PSNR breach) triggered the existing fallback to the calibrated,
non-DE parallel-screenshot path; any other capture-stage failure on a
pinned render just hard-failed the whole job instead of reusing that same
tested safety net.

shouldRetryViaPinnedFallback widens the retry to any capture failure while
deWorkerInversion="inverted" or deParallelRouter="routed", excluding OOM
(the fallback's worker count can be >= the pinned count, so retrying would
likely just OOM again — fail fast instead).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@vanceingalls

Copy link
Copy Markdown
Collaborator Author

Added a second commit based on live testing this afternoon.

While dogfooding the router with a real render (HF_DE_PARALLEL_ROUTER=true, workers left auto), calibration on this machine picked 5 workers — the router still pinned to 3 and it worked fine, but it surfaced a real gap: the router/inversion pin a fixed worker count regardless of calibration or host load, and I separately watched this same machine's load spike to 12+ from an unrelated process mid-benchmark. If a capture-stage failure (timeout, worker crash) happens on that pinned count under contention, today it just hard-fails the whole render — only a drawElement self-verify failure (blank/PSNR) gets the existing fallback to the calibrated, non-DE parallel-screenshot path.

df57eb0fd widens that retry: shouldRetryViaPinnedFallback now retries via the same tested fallback for any capture failure while deWorkerInversion="inverted" or deParallelRouter="routed", not just verify failures — reusing the exact retry machinery already built and tested for #2026/#2095, just broadening the trigger.

Deliberately excluded: OOM. The fallback's worker count is calibration's own pick, which can be higher than the pin (my dogfood run: calibration wanted 5, router pinned to 3) — retrying at the same or higher count would likely just OOM again, so that still fails fast instead of masking a real resource problem behind a doomed retry.

7 new unit tests on the extracted predicate, 122/122 total pass, typecheck/lint/format clean.

…e-pinned count

shouldRetryViaPinnedFallback retrying OOM was only half the fix: it reused
preInversionWorkerCount/preRouterWorkerCount unmodified, which is
calibration's own pick and can be >= the pinned count that just OOM'd
(calibration wanting 5 while the router pinned to 3). Retrying at equal or
higher parallelism than the failure isn't a remedy — it's the same bet
again, and worsens the odds for this render and anything sharing the host
(PRODUCER_MAX_CONCURRENT_RENDERS runs concurrent jobs in one process).

resolveInversionRetryPlan/resolveParallelRouterRetryPlan now drop to
workerCount=1 specifically when the retry is OOM-triggered — one Chrome
page, the leanest configuration available, not just a different capture
mode at the same worker count. Ordinary self-verify (blank/PSNR) retries
are unaffected — those aren't memory-related, so they keep the
pre-inversion/pre-router count as before.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@vanceingalls

Copy link
Copy Markdown
Collaborator Author

Fixed a real gap in the OOM-retry commit — fair pushback: reusing `preInversionWorkerCount`/`preRouterWorkerCount` unmodified on an OOM retry was retrying at the same or higher parallelism than what just OOM'd (calibration can pick more workers than the pin, e.g. 5 vs the router's 3) — that's not a remedy, it's the same bet again, and worse for anything else sharing the host process.

`51353a7ed`: `resolveInversionRetryPlan`/`resolveParallelRouterRetryPlan` now drop to `workerCount=1` specifically when the retry is OOM-triggered — one Chrome page, the actual leanest configuration available, not just a different capture mode at the same worker count. Ordinary self-verify (blank/PSNR) retries are untouched — those aren't memory-related, so they keep behaving exactly as before.

2 new tests covering the OOM-drops-to-1 case for both retry-plan functions, 124/124 total pass.

…haustionError

Found while testing the previous commit's OOM-drops-to-1-worker fallback
end-to-end: the producer's deployed runtime is Bun (JavaScriptCore), not
Node (V8) — see packages/gcp-cloud-run/Dockerfile's `bun dist/server.js`
entrypoint. All 7 MEMORY_EXHAUSTION_ERROR_PATTERNS are V8-specific allocation
failure signatures; JSC's equivalent for the same single-oversized-allocation
RangeErrors is the bare string "Out of memory" (verified against real Bun
behavior), which none of them match. Without this, isMemoryExhaustionError
returns false for genuine production OOM, so the memory-specific worker-count
reduction just added would never actually engage where it's deployed — every
OOM would fall through to the generic capture_error retry path instead.

Matches the FULL (trimmed) message only, not merely a substring — same
rationale as the existing V8 patterns' comment: "out of memory" also appears
in benign WebGL/GPU console noise that must not trip this classifier.

Verified end-to-end from a script inside the producer workspace (importing
the real @hyperframes/engine source, not a stale globally-cached npm dist a
script outside the workspace would otherwise resolve to): a genuine Bun
RangeError from new Uint8Array(Number.MAX_SAFE_INTEGER) now correctly
classifies as memory exhaustion and drives both resolveInversionRetryPlan
and resolveParallelRouterRetryPlan down to workerCount=1 on retry.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@vanceingalls

Copy link
Copy Markdown
Collaborator Author

You asked if I actually tested the OOM-drops-to-1 fix — I hadn't beyond unit tests, so I went and did it live. Good thing I did: the fix as shipped would never have engaged in production.

`isMemoryExhaustionError` only matches V8-specific error text (`"JavaScript heap out of memory"`, `"Set maximum size exceeded"`, etc.) — but the producer's deployed runtime is Bun/JavaScriptCore (`packages/gcp-cloud-run/Dockerfile`: `CMD ["bun", "dist/server.js"]`), not Node/V8. I forced a real catchable OOM under Bun (`new Uint8Array(Number.MAX_SAFE_INTEGER)`) and the actual thrown message is the bare string `"Out of memory"` — none of the 7 existing patterns match it. So in the real deployed runtime, `isMemoryExhaustion` would always resolve `false`, and the whole worker-count-reduction from the previous commit would silently never fire — every OOM would just fall through to the generic `capture_error` retry (still retries, per the widened fallback, but without ever dropping to 1 worker).

Fixed in `b3f244a7e`: added an exact-message match for Bun's `"Out of memory"` (trimmed, case-insensitive) — deliberately an EXACT match, not a substring check, same reasoning as the existing patterns' comment (a bare "out of memory" substring shows up in benign WebGL/GPU console noise and must not misclassify).

Also worth flagging while I was testing: a genuine full-heap-exhaustion OOM (unbounded growth until the VM's hard limit) is not catchable at all — I forced one with `bun --max-old-space-size=64` and it SIGKILLs the process (exit 137) before any `catch` runs, no retry code ever executes. So this whole OOM-remedy path only ever helps the catchable flavor — a single oversized allocation hitting RangeError/Out of memory — which is the realistic failure mode for a large frame buffer, not a slow accumulating leak. Worth knowing as a ceiling on what this fix (and any future OOM handling) can do.

Verified end-to-end (not just unit tests) from a script inside the producer workspace, importing the real workspace source: real Bun `RangeError` → `isMemoryExhaustionError` true → `shouldRetryViaPinnedFallback` true → both `resolveInversionRetryPlan` and `resolveParallelRouterRetryPlan` correctly resolve `workerCount: 1`. Also confirmed the "no pinned cohort" case correctly still fails fast (no false-positive retry).

39/39 tests pass on the classifier, 124/124 on the orchestrator, typecheck/lint/format clean.

… gaps

Three defects found by max-effort code review of this branch:

1. The Bun OOM exact-match regex was defeated by this codebase's own
   parallel-worker error wrapping. executeParallelCapture/formatWorkerFailure
   (parallelCoordinator.ts) always wrap a worker's error as
   "Worker N: <message>", optionally suffixed and joined with other workers'
   segments, all prefixed "[Parallel] Capture failed: ". That wrapping
   defeated the exact-message check for exactly the cohort (deParallelRouter
   routed, N separate Chrome processes) the OOM-drops-to-1 fix targets — a
   real OOM there would retry at the SAME worker count instead of dropping
   to 1. Added a second pattern that recovers the signal by requiring
   "out of memory" appear as the WHOLE content of a "Worker N: ..." segment
   (bounded by end-of-string/"; "), preserving the same exact-match property
   (no bare substring match) while surviving the wrapping. Verified against
   the real wrapping logic, not a hand-typed guess at its shape.

2. shouldRetryViaPinnedFallback didn't exclude cancellation, so aborting a
   render mid-capture on the pinned router/inversion cohort would detour
   through spawning a fresh encoder/capture session before the outer catch's
   RenderCancelledError branch ended the render — delaying "stop" with a
   pointless resource spin-up/tear-down. Added an isCancellation param
   (checked first, before isVerifyError) using the same
   `err instanceof RenderCancelledError || abortSignal?.aborted` check the
   outer catch already uses.

3. deFallbackReason (this PR's new "oom"/"capture_error" values) was set
   locally but never mirrored into RenderCaptureObservability alongside
   deSelfVerifyFallback, so a render that fails AFTER a fallback attempt
   (perfSummary never built) was indistinguishable in render_error telemetry
   from one that never attempted any fallback — undercutting the "how often
   does the OOM retry fire on a render that still ultimately fails"
   question this branch exists to answer. Threaded through
   RenderCaptureObservability → RenderObservabilityTelemetryPayload →
   renderObservabilityTelemetryPayload, mirroring the existing
   deSelfVerifyFallback plumbing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@vanceingalls

Copy link
Copy Markdown
Collaborator Author

Ran a max-effort automated code review of this branch (17 finder/verifier agents). 3 findings survived verification, all fixed in `a355fb2f6`:

1. The Bun OOM regex was defeated by this codebase's own error wrapping. `executeParallelCapture`/`formatWorkerFailure` (`parallelCoordinator.ts`) wrap a worker's error as `"[Parallel] Capture failed: Worker N: "` before it ever reaches `isMemoryExhaustionError` — the exact-match check from the previous commit never survives that wrapping, so a real OOM on the router-routed cohort (the exact case the drop-to-1 fix targets) would have retried at the SAME worker count that just OOM'd. Added a second pattern that recovers the signal by requiring `"out of memory"` be the WHOLE content of a `"Worker N: ..."` segment (bounded by end-of-string or `"; "`) — preserves the same no-bare-substring-match property, survives the wrapping. Verified against the actual wrapping logic (not a guess), and re-verified end-to-end from inside the workspace with the exact wrapped message string, confirming `workerCount: 1` now comes out correctly.

2. Cancellation wasn't excluded from the retry. `shouldRetryViaPinnedFallback` would have caught a mid-render `RenderCancelledError` on the pinned cohort and detoured through spawning a fresh encoder/capture session before the outer catch's cancellation branch ever ran — delaying "stop" with a pointless resource cycle. Added `isCancellation` (checked first), using the same `err instanceof RenderCancelledError || abortSignal?.aborted` check the outer catch already uses.

3. `deFallbackReason` never reached failure-path telemetry. Set locally but never mirrored into `RenderCaptureObservability`, so a render that fails AFTER attempting a fallback was indistinguishable from one that never attempted any fallback — directly undercutting this branch's own stated goal of measuring how often the OOM retry fires on a render that still ultimately fails. Threaded through the same path as the existing `deSelfVerifyFallback`.

10 new tests added across the 3 fixes, 199/199 pass, typecheck/lint/format clean.

vanceingalls and others added 3 commits July 9, 2026 21:40
…emetry

HF_DE_PARALLEL_ROUTER is a producer env var with no self-serve opt-in path
for real users, so waiting for someone to manually enable it would never
produce the real-traffic telemetry (revert rate, verify-db distribution)
the router's soak plan calls for.

renderLocal now enables the experiment for free on a fresh install's CLI
renders until it actually engages once (routed or reverted — either
produces telemetry), then persists that to ~/.hyperframes/config.json and
never touches it again for that install. A render whose frame count never
crosses the router's own eligibility threshold doesn't consume the trial —
it stays available for a later render that does qualify.

Never overrides a user's own explicit HF_DE_PARALLEL_ROUTER setting, and
only engages when telemetry is enabled (no point risking the experimental
path if we can't record the resulting signal). Scoped to the in-process CLI
render path only — Docker renders don't thread perfSummary/errorDetails
back to the CLI process, so trial consumption can't be detected there.

Verified the config round-trip against a real file (fresh install ->
undefined -> write true -> persists across reread), not just the mocked
unit tests.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…not first engagement

Only consuming telemetry from one data point per install badly undersampled
the "routed" (successful) outcome — the far more common case. Changed
maybeConsumeDeParallelRouterTrial to only turn the trial off when the
router's OWN safety net actually fired (deParallelRouter === "reverted"),
not on a clean "routed" success. This runs the experiment on every eligible
render for an install indefinitely until it hits one real failure, then
stops for that install going forward — trading a slightly higher per-install
ceiling on experimental-path exposure for dramatically more successful-
routing telemetry volume across the fleet.

Also fixed a related edge case while updating this: a render that merely
"routed" (router fired, self-verify never even tripped) but then crashed
for an unrelated reason (e.g. cancellation) no longer counts as a router
failure — only "reverted" (the router's fallback path actually engaged)
does. Cancelling a render isn't evidence the router is unsafe.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…dTrack gap in DE trial

Four confirmed findings from a max-effort code review of the CLI trial
mechanism:

1. maybeEnableDeParallelRouterTrial's `process.env.HF_DE_PARALLEL_ROUTER
   !== undefined` guard couldn't distinguish "the user set this" from "an
   earlier renderLocal() call in this same process already armed it" — so
   in --batch (all rows share one process), only row 1's outcome could
   ever reach maybeConsumeDeParallelRouterTrial. A revert on any later row
   was silently never persisted. Added a module-level
   deParallelRouterTrialManagedByUs flag to disambiguate, with a test-only
   reset export since it's process-lifetime state a real CLI invocation
   never needs to reset but a test suite sharing one module instance does.

2. writeConfig is a non-atomic whole-file overwrite with no locking, and
   readConfig's cache never invalidates — a concurrently running second
   CLI process (another terminal, a parallel script; doesn't even need to
   be a render, any command calls incrementCommandCount) could silently
   clobber a just-persisted deParallelRouterTrialFired:true with its own
   stale snapshot. Added readConfigFresh (bypasses the cache) and use it
   immediately before the trial's read-modify-write, narrowing the race
   window without a full config-subsystem locking rewrite.

3. The prior commit's semantics flip removed the only exposure cap — a
   healthy router that never reverts now force-enabled the experimental
   path on every eligible render forever. Added
   DE_PARALLEL_ROUTER_TRIAL_MAX_RENDERS (25) as a backstop: the trial turns
   off after this many engaged renders even absent an actual failure.

4. maybeEnableDeParallelRouterTrial only checked config.telemetryEnabled,
   not shouldTrack() — so a dev-mode run or a DO_NOT_TRACK/
   HYPERFRAMES_NO_TELEMETRY user got the experimental path silently armed
   while telemetry was simultaneously blocked underneath it. Now gates on
   shouldTrack() (a strict superset).

Also fixed, lower severity: readConfig's deParallelRouterTrialFired/
deParallelRouterTrialRenderCount parsing now validates the JSON type
explicitly instead of a bare truthy/nullish read, so a hand-edited or
corrupted config can't have the string "false" misread as truthy.

Refactored maybeEnableDeParallelRouterTrial into three smaller functions
(isDeParallelRouterTrialBlocked, stopManagingDeParallelRouterTrial) to
bring cyclomatic/cognitive complexity back under the repo's threshold —
also de-duplicates the "stop managing the env var" logic shared with
maybeConsumeDeParallelRouterTrial.

14 new/updated tests (43 total in render.test.ts), including a direct
regression test for the batch re-entrancy scenario and a loop test for the
render-count cap. Verified the config primitives end-to-end against a real
file, not just the mocked unit tests.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@vanceingalls

Copy link
Copy Markdown
Collaborator Author

Ran a second max-effort code review focused on the CLI trial mechanism (last 2 commits, unreviewed at the time). 4 confirmed findings, all fixed in `532dad7cc`:

1. Batch re-entrancy bug (highest severity). `maybeEnableDeParallelRouterTrial`'s guard (`process.env.HF_DE_PARALLEL_ROUTER !== undefined`) couldn't distinguish "the user set this" from "row 1 of this same `--batch` run already armed it." Every row after the first silently got `trialArmed=false`, so a real revert on row 2+ was never persisted — permanently defeating the "one real failure turns it off" guarantee for that install. Fixed with a module-level flag that tracks whether we set the env var, distinct from its current value, plus a direct regression test reproducing the exact two-row scenario.

2. Config file race across concurrent CLI processes. `writeConfig` is a non-atomic whole-file overwrite with an unbounded read cache — a second concurrently-running CLI process (even just any other command, since all of them call `incrementCommandCount`) could clobber a just-persisted `deParallelRouterTrialFired: true` with its own stale snapshot. Added `readConfigFresh` (bypasses the cache) and use it immediately before the trial's read-modify-write, narrowing the race window without a full locking rewrite.

3. Regression: no exposure cap. The previous commit's semantics flip (only "reverted" turns it off) removed the one-render cap entirely — a healthy router that never reverts would stay force-enabled forever. Added `DE_PARALLEL_ROUTER_TRIAL_MAX_RENDERS = 25` as a backstop.

4. Missing `shouldTrack()` gate. Only checked `config.telemetryEnabled`, not `shouldTrack()` — so a dev-mode run or `DO_NOT_TRACK`/`HYPERFRAMES_NO_TELEMETRY` user got the experimental path silently armed while telemetry was simultaneously blocked underneath it. Now gates on `shouldTrack()` (a strict superset).

Also fixed a lower-severity stringly-typed-boolean parsing gap, and refactored the enable function into smaller pieces to clear the repo's own complexity gate (which the added logic had pushed over threshold).

14 new/updated tests (43 total in `render.test.ts`), verified the config primitives end-to-end against a real file, typecheck/lint/format/fallow-audit all clean.

…trial gaps

Six findings from a third max-effort code review, focused on the previous
commit's fixes:

1. --batch-concurrency N>=2 runs genuinely concurrent renderLocal() calls
   (Promise.all workers in batchRender.ts), which can't safely share the
   trial's one process-wide env var + module flag — a row finishing first
   could tear down the env var/flag mid-render for a sibling row still in
   flight. Rather than attempt to make shared process-global state safe
   under real concurrency, added RenderOptions.disableDeParallelRouterTrial
   and set it whenever batchConcurrency > 1 — the trial simply isn't
   offered when it can't be evaluated safely.

2. maybeConsumeDeParallelRouterTrial's "outcome === undefined" no-op guard
   almost never fired: aggregateDrawElement (perfSummary.ts) defaults
   parallelRouter to the string "none" for every render, whether or not
   drawElement/the router ever engaged — never undefined. Every ordinary
   render below the router's own frame threshold (the common case) was
   ticking the render-count backstop, tripping
   DE_PARALLEL_ROUTER_TRIAL_MAX_RENDERS after 25 completely unrelated
   renders that never touched the router. Now treats "none" the same as
   undefined.

3. isDeParallelRouterTrialBlocked relied solely on shouldTrack(), which
   memoizes its verdict once per process — during a long --batch run, a
   `hyperframes telemetry off` issued from another terminal mid-batch would
   never be observed. Restored a direct config.telemetryEnabled check
   (read fresh every call, unlike shouldTrack()'s cache) alongside it.

4. maybeConsumeDeParallelRouterTrial's config write had no way to detect a
   losing race against a concurrent process — added a verify-and-retry
   loop (write, re-read fresh, retry up to 3x if a concurrent writer
   landed in between) that narrows the window further without a full
   file-locking rewrite.

5. The trial could arm before the first-run telemetry disclosure
   (showTelemetryNotice) was guaranteed to have printed — that notice runs
   via a fire-and-forget, unawaited dynamic import in cli.ts with no
   ordering guarantee relative to the render command. Rather than touch
   that pre-existing async bootstrap chain, gated the trial on
   config.telemetryNoticeShown: it simply never offers itself on a fresh
   install's very first invocation.

6. Added a dedicated config.test.ts exercising readConfig/readConfigFresh/
   writeConfig through the REAL module (node:fs mocked with an in-memory
   fake, not a HOME-env hack) — readConfigFresh's cache-bypass and the
   type-guarded boolean/number parsing had zero coverage through the real
   implementation before this.

Also fixed the test fixture that was supposed to cover finding #2 but used
an unrealistic `drawElement: {}` shape instead of the real
`{ parallelRouter: "none" }` aggregateDrawElement actually produces.

Extracted applyDeParallelRouterOutcome to keep maybeConsumeDeParallelRouterTrial
under the repo's complexity gate after adding the retry loop.

11 new/updated tests in render.test.ts (56 total) + 7 new tests in
config.test.ts. Verified against fallow's audit gate clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@vanceingalls

Copy link
Copy Markdown
Collaborator Author

Third max-effort review, focused on the previous commit's own fixes. 6 confirmed findings, all fixed in `dc6df93de`:

1. `--batch-concurrency N>=2` breaks the re-entrancy fix. Real concurrent `renderLocal()` workers (via `Promise.all` in `batchRender.ts`) can't safely share one process-wide env var + module flag — a row finishing first could tear down the env var mid-render for a sibling row still in flight. Rather than try to make shared global state safe under true concurrency, added `RenderOptions.disableDeParallelRouterTrial` and set it whenever `batchConcurrency > 1` — the trial just isn't offered when it can't be evaluated safely.

2. The `outcome === undefined` no-op guard almost never fired. `aggregateDrawElement` defaults `parallelRouter` to the string `"none"` for every render, never `undefined` — so ordinary renders below the router's frame threshold (the common case) were ticking the render-count backstop and tripping the 25-render cap after zero real router engagements. Now treats `"none"` the same as `undefined`. Also fixed the test fixture that was supposed to cover this but used an unrealistic shape.

3. `shouldTrack()`'s process-lifetime memoization vs. a long batch. If a user runs `hyperframes telemetry off` in another terminal mid-batch, the cached `shouldTrack()` verdict would never reflect it. Restored a direct `config.telemetryEnabled` check (read fresh every call) alongside `shouldTrack()`.

4. Config write race could still flip `fired` back to `false`, not just lose an increment. Added a verify-and-retry loop (write, re-read fresh, retry up to 3x if a concurrent writer landed in between) — narrows further without a full file-locking rewrite.

5. Trial could arm before the first-run telemetry disclosure was guaranteed to have printed (that notice runs via an unawaited dynamic import in `cli.ts`). Rather than touch that pre-existing bootstrap chain, gated the trial on `config.telemetryNoticeShown` — it simply never offers itself on a fresh install's very first invocation.

6. `readConfigFresh` and the type-guarded parsing had zero coverage through the real implementation. Added a dedicated `config.test.ts` (node:fs mocked with an in-memory fake, not a HOME-env hack) exercising the actual module.

11 new/updated tests in `render.test.ts` (56 total) + 7 new in `config.test.ts`, typecheck/lint/format/fallow-audit all clean.

…onfig re-arm in DE trial

Three root causes from a fourth max-effort review (15 raw findings deduped;
the synthesize step died on a session limit so they arrived unmerged):

1. The previous commit's telemetryEnabled fix was ineffective: the arm site
   passed readConfig() — the process-lifetime cache — into
   isDeParallelRouterTrialBlocked, making it exactly as stale as the
   shouldTrack() memoization it claimed to bypass. A mid-batch
   `hyperframes telemetry off` (or another process persisting fired=true)
   was never observed. Now reads readConfigFresh() at the arm site; the
   test mock previously hid this because readConfig/readConfigFresh were
   behaviorally identical views over one shared object.

2. The verify-and-retry write loop double-counted a render whenever OUR
   write landed but a concurrent writer advanced the file before our
   verify read — the retry re-applied the increment on top (two renders
   → three counts), tripping the 25-render exposure cap early and
   permanently killing the trial with less telemetry than the cap was
   designed to allow. Reworked: the render COUNTER is written exactly
   once, unverified (a lost increment under-counts by one — benign); only
   the FIRED flag is verified and re-asserted, which is idempotent, so
   retries can no longer corrupt anything
   (persistDeParallelRouterTrialFired).

3. writeConfig swallows all fs errors, so on an unwritable ~/.hyperframes
   a reverted outcome could never persist — the trial would re-arm and
   re-fail on every subsequent render forever, silently. Added an
   in-process fired latch (set at decision time, before persistence is
   attempted) consulted by the blocked-check, plus a one-time console
   warning when persistence exhausts its attempts. Later processes still
   re-arm (disk is the only cross-process channel), but each process now
   stops after at most one failure it couldn't record.

Test infrastructure fix enabling all of the above to be tested: the config
mock now models disk vs cache SEPARATELY (readConfig serves the cache,
readConfigFresh re-reads "disk", writeConfig updates both) with a
failWrites hook simulating the real writeConfig's silent error swallowing.
The old single-shared-object mock made cached-vs-fresh mis-routing and
retry iterations untestable by construction.

3 new regression tests: mid-batch opt-out observed through the cache;
fired flag re-asserted after a lost write WITHOUT re-counting the render;
unwritable-config latch blocking re-arm. 56 tests total across
render.test.ts + config.test.ts.

Not fixed (by design): the widened pinned-fallback retry paying a doubled
render on deterministic mid-stream failures (e.g. ENOSPC) — the accepted
tradeoff of the fallback design; cancellation and OOM are special-cased.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vanceingalls

Copy link
Copy Markdown
Collaborator Author

Fourth max-effort review round (15 raw findings, deduped to 5 root causes — the review's own synthesize step died on a session limit, so dedup was manual). All real ones fixed in `2542e9427`:

1. The previous telemetryEnabled fix was ineffective (5 findings, one root cause). The arm site passed `readConfig()` — the process-lifetime cache — into the blocked-check, making it exactly as stale as the `shouldTrack()` memoization it claimed to bypass. Mid-batch `hyperframes telemetry off` was still never observed. Now reads `readConfigFresh()` at the arm site. The prior test passed only because the mock made `readConfig`/`readConfigFresh` behaviorally identical — see fix 4.

2. Verify-and-retry loop double-counted renders (5 findings, one root cause). When our write landed but a concurrent writer advanced the file before our verify read, the retry re-applied the increment — two renders → three counts → exposure cap trips early → trial permanently killed with less telemetry than designed. Reworked: the counter is written exactly once, unverified (lost increment = benign undercount); only the fired flag is verified/re-asserted, which is idempotent so retries can't corrupt anything.

3. Unwritable config = silent infinite re-arm (2 findings). `writeConfig` swallows all fs errors, so a reverted outcome could never persist on a read-only `~/.hyperframes` — re-arm and re-fail forever. Added an in-process fired latch (set at decision time, before persistence is attempted) + a one-time warning when persistence exhausts. Later processes still re-arm (disk is the only cross-process channel), but each process stops after at most one unrecordable failure.

4. Test mock erased the cache-vs-disk distinction (2 findings). `readConfig`/`readConfigFresh`/`writeConfig` were three views over one shared object, making findings 1 and 2 untestable by construction. The mock now models disk vs cache separately with a `failWrites` hook simulating the real `writeConfig`'s silent error swallowing. 3 new regression tests cover the mid-batch opt-out, the fired-flag re-assert-without-recount, and the unwritable-config latch.

5. Not fixed, by design: one finding flagged that the widened pinned-fallback retry (from the earlier producer commits) pays a doubled render on deterministic mid-stream failures like ENOSPC before surfacing the error. That's the accepted tradeoff of the fallback design discussed earlier in this PR — cancellation and OOM are already special-cased; further error-classification heuristics aren't worth the whack-a-mole.

56 tests across `render.test.ts` + `config.test.ts`, typecheck/lint/format/fallow-audit clean.

…e signal

Five findings from a fifth (final scoped) max-effort review of the previous
commit, all local:

1. writeConfig now writes atomically (pid-suffixed temp file + renameSync —
   rename within one directory is atomic on POSIX). This closes the real
   hazard behind the review's torn-read finding: readConfig's corrupted-file
   catch RESETS the config to defaults (telemetry re-enabled, anonymousId
   rotated, trial fields wiped), so a concurrent reader catching a
   non-atomic write mid-flight would silently destroy the user's config —
   and the previous commit's per-render readConfigFresh() at the arm site
   multiplied exposure to exactly that window. Verified against a real
   filesystem, not just the mocked unit tests.

2. writeConfig now returns whether the write landed (errors still swallowed
   — telemetry must never break the CLI). persistDeParallelRouterTrialFired
   uses it to stop immediately on a genuine fs failure (retrying an
   unwritable file is pointless) and reserve its retries for actual
   concurrent clobbers, instead of 3 blind write attempts + 4 disk reads.

3. The persistence-failure console.warn is now !quiet-gated like every
   other trial message — a quiet/batch-json render on an unwritable
   ~/.hyperframes no longer emits unexpected stderr that CI wrappers
   asserting empty stderr would misread as a render failure. The in-process
   latch already guarantees the safety behavior whether or not the warning
   prints.

4. The arm site short-circuits on the in-process fired latch BEFORE the
   fresh config read — post-fired batch rows no longer pay a per-row config
   read + parse + shared-cache invalidation for an answer module state
   already knows.

5. Replaced the new `as T` assertions in render.test.ts's config-state
   factory with an explicitly typed vi.hoisted return (repo TypeScript
   convention: no `as T`).

config.test.ts: node:fs mock gains renameSync (faithful to the new atomic
write); new test covers the success/failure return and asserts no temp file
survives a write.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vanceingalls

Copy link
Copy Markdown
Collaborator Author

Fifth (final scoped) review round, focused solely on the previous commit. 5 findings, all local — none structural, so per the pre-committed exit rule this stays a patch, and it's the last one. Fixed in `6172d79dc`:

1. Atomic config writes (the round's most consequential find). `writeConfig` was a plain `writeFileSync` — not atomic — while `readConfig`'s corrupted-file catch RESETS the whole config to defaults (telemetry re-enabled, anonymousId rotated, trial off-switch wiped). A concurrent reader catching a write mid-flight would silently destroy the user's config, and the previous commit's per-render `readConfigFresh()` multiplied exposure to exactly that window. Now writes to a pid-suffixed temp file + `renameSync` (atomic within a directory on POSIX). Verified against a real filesystem.

2. `writeConfig` reports success. Errors still swallowed (telemetry must never break the CLI), but `persistDeParallelRouterTrialFired` now stops immediately on a genuine fs failure instead of 3 blind writes + 4 reads, reserving retries for actual concurrent clobbers — the distinction the previous design couldn't make.

3. The persistence-failure warning is now `!quiet`-gated like every other trial message — no more unexpected stderr on quiet/batch-json renders that strict CI wrappers would misread as failure. The in-process latch already guarantees the safety behavior either way.

4. Arm site short-circuits on the in-process latch before the fresh disk read — post-fired batch rows no longer pay per-row config I/O for an answer module state already knows.

5. Removed the new `as T` assertions in the test factory (repo TS convention) via an explicitly typed `vi.hoisted` return.

57 tests across render.test.ts + config.test.ts (one new: writeConfig success/failure return + no-temp-file-left-behind), typecheck/lint/format/fallow clean, real-fs verification of the atomic write and the read-only-dir failure path.

@miga-heygen miga-heygen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

SSOT Review — #2115 feat(producer,cli): full telemetry visibility for DE parallel-router/inversion failures

What this PR does

Six interlocking changes, each building on the last:

  1. Failure-path telemetry visibilityRenderCaptureObservability now records dePreInversionWorkers/dePreRouterWorkers/deFallbackReason so a hard failure while routed/inverted is distinguishable from an unrelated crash in PostHog.
  2. Widen self-verify retryshouldRetryViaPinnedFallback (new, pure, unit-tested) extends the existing retry path to any capture-stage failure on a pinned worker count, not just drawElement self-verify trips.
  3. OOM drops to single workerresolveInversionRetryPlan/resolveParallelRouterRetryPlan accept isMemoryExhaustion and drop to workerCount=1 when true (the actual memory remedy, not the same-or-higher count).
  4. Bun OOM recognitionisMemoryExhaustionError now matches JSC's bare "Out of memory" AND the same message wrapped by this codebase's own executeParallelCapture error formatting.
  5. Three confirmed code-review fixes — cancellation exclusion in retry path, deFallbackReason threading to failure telemetry, wrapped-worker regex for Bun OOM.
  6. CLI trialmaybeEnableDeParallelRouterTrial/maybeConsumeDeParallelRouterTrial enable the router on every eligible CLI render for fresh installs, turning off only on a real safety-net revert (or a 25-render backstop cap).

SSOT analysis

Ran the full 7-step loop against the diff:

Helpers — every new function carries a fact the caller doesn't know:

  • shouldRetryViaPinnedFallback owns the retry-eligibility decision (cancellation exclusion, verify-error always, pinned-cohort generic failures). Called exactly once in the catch block.
  • isDeParallelRouterTrialBlocked owns the full blocking criteria (fired flag, render cap, telemetry state, notice shown). No duplication with the arm site.
  • resolveDeParallelRouterOutcome normalizes "none"undefined — this normalization is required (documented: without it, ordinary sub-threshold renders would tick the render counter).
  • persistDeParallelRouterTrialFired owns the verify-and-re-assert retry loop — idempotent on the boolean flag, deliberately does NOT re-assert the render counter (double-counting would trip the cap early).

No duplicated decisions found. The retry decision lives in shouldRetryViaPinnedFallback only. The trial-gating decision lives in isDeParallelRouterTrialBlocked only. The OOM detection lives in isMemoryExhaustionError only.

Side-effect invariant:

Condition Actor Side Effect Expected Count
Trial eligible + not blocked maybeEnableDeParallelRouterTrial Sets process.env.HF_DE_PARALLEL_ROUTER Once per render
Router engaged (routed/reverted) maybeConsumeDeParallelRouterTrial Increments deParallelRouterTrialRenderCount Once per render, fire-and-forget
Router reverted OR cap hit maybeConsumeDeParallelRouterTrialpersistDeParallelRouterTrialFired Writes deParallelRouterTrialFired: true Once, verified with up to 3 retry attempts
Trial over stopManagingDeParallelRouterTrial Deletes env var + clears managedByUs flag Once per render

No double mutations, no missed paths. Both success and error paths call maybeConsumeDeParallelRouterTrial — verified in the diff.

Sentinel-value scan:

  • deParallelRouterTrialFired parsed with === true (not truthy) — guards against string "false" in hand-edited JSON. ✓
  • deParallelRouterTrialRenderCount parsed with typeof === "number". ✓
  • resolveDeParallelRouterOutcome normalizes "none"undefined. ✓
  • ?? 0 for render count is correct (undefined means 0). ✓

Stale concept grep: No remnants of earlier designs visible. The old isDrawElementVerificationError-only catch was fully replaced by the shouldRetryViaPinnedFallback gate.

Code observations

The writeConfig atomicity fix is a quiet gem. The temp-file + renameSync change protects against a scenario the comment identifies: readConfig's corrupted-JSON catch resets to defaults (new anonymousId, all optional fields wiped), so a torn read of a non-atomic write would silently destroy the user's config. Good defensive change that benefits all config consumers, not just the trial.

The readConfigFresh vs readConfig distinction is well-motivated. The arm site and consume site both use readConfigFresh because readConfig's process-lifetime cache is exactly as stale as shouldTrack()'s memoization — a mid-batch hyperframes telemetry off from another terminal would never be observed. The short-circuit on deParallelRouterTrialFiredThisProcess (before the disk read) is a nice optimization for post-fired batch rows.

The Bun OOM regex pair is carefully scoped. BUN_MEMORY_EXHAUSTION_EXACT_MESSAGE matches the bare message (/^out of memory\.?$/i), while BUN_MEMORY_EXHAUSTION_WRAPPED_WORKER_MESSAGE matches through executeParallelCapture's "Worker N: <message>" wrapping — requiring "out of memory" as the whole worker-segment content ((?:;|$) boundary), not a substring. The negative test case ("Worker 2: WebGL context lost, out of memory reported by driver") confirms the substring-exclusion property holds.

The disableDeParallelRouterTrial option for batchConcurrency > 1 is the right call. The process-wide env var + module-level flag are safe for sequential batch rows but not genuinely concurrent ones. Rather than making shared mutable state safe under concurrency, just don't offer the trial. Simple and correct.

Test coverage is excellent — 40+ new tests covering the full CLI trial matrix (fresh install, already fired, telemetry off, notice not shown, user-set env var, clean routed success, reverted, unrelated crash, batch row sequencing, concurrency disable, mid-batch opt-out, lost writes, unwritable config, render cap), OOM detection (Bun bare + wrapped + negative), retry logic (shouldRetryViaPinnedFallback verify/generic/OOM/cancellation/reverted-cohort), and config module behavior (cache/fresh/write/corrupt/atomic).

Verdict

No SSOT violations. No stale plumbing. No sentinel-value issues. The failure-mode table in the PR description is thorough and the "known gaps" are honest. The code-review findings are already incorporated. CI is 46/47 green (one regression shard still running).

This is a well-structured hardening PR. Ship it.

— Miga

@james-russo-rames-d-jusso james-russo-rames-d-jusso left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Reviewed at 6172d79dc223.

Clean, high-effort PR — the failure-mode matrix in the body reads like a design doc, and 5 self-review rounds already scrubbed the interior. The retry-plan predicates (shouldRetryViaPinnedFallback, resolveInversionRetryPlan, resolveParallelRouterRetryPlan) are all pure and tested end-to-end (verify / generic / OOM / cancellation matrix); deParallelStreamForced correctly threaded as a per-render local instead of process.env mutation closes real cross-render cross-talk in the concurrent-producer scenario; Bun OOM regex handles the wrapped-worker case as exact-match-per-segment (verified against the substring-negative test); CLI trial mechanics (in-process latch surviving unwritable disk, readConfigFresh bypassing the stale-cache bug, resolveDeParallelRouterOutcome normalizing "none"undefined to avoid ticking the render-count cap on unrelated renders) all handle the concurrency / persistence edge cases the self-review rounds surfaced.

Two non-blocking notes inline:

  1. deSelfVerifyFallback semantic narrowing — the field used to effectively mean any fallback fired; with the widened retry it now means verify-triggered specifically. Downstream PostHog dashboards keyed on that as any fallback will silently under-count OOM / capture_error retries. Coordinate with the observability rebuild.
  2. Trial disable is opt-out from CLI batch code onlyrenderLocal is exported; any programmatic consumer sharing the process picks up the trial + its module-level latch. Doc-comment could name the assumption explicitly, or invert to opt-in.

The one gap I'd flag structurally is host-load / contention awareness before routing (called out in the PR body's Known gaps) — accepting that as noted, since the retry safety net catches it reactively.

Review by Rames D Jusso

)
throw err;
const isMemoryExhaustion = !isVerifyError && isMemoryExhaustionError(err);
deSelfVerifyFallback = isVerifyError;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 Downstream metric semantic narrowing. Before this PR, this line ran unconditionally inside the verify-error branch, so de_self_verify_fallback = true was effectively any DE fallback fired. With the widened retry, this flips to deSelfVerifyFallback = isVerifyError, and OOM / capture_error-triggered fallbacks now emit de_self_verify_fallback = false while carrying de_fallback_reason ∈ {oom, capture_error}.

Any PostHog dashboard / alert / cohort filter that used de_self_verify_fallback = true as the any fallback fired signal will silently under-count post-merge — exactly the failure mode this PR exists to close, but for the observability side rather than the code side. Given this is the very cohort the PR is expanding failure-path visibility for, worth either:

  1. Preserving deSelfVerifyFallback = true for any fallback (keep the compat, downgrade the field to just "fallback fired" and let deFallbackReason carry the sub-reason), or
  2. Explicitly calling out in the PR body that dashboards should migrate to de_fallback_reason IS NOT NULL — Miguel's observability rebuild (project_hf_observability_rebuild) is the natural coordination point here.

The name deSelfVerifyFallback also reads narrower than the field's original meaning now — a follow-up rename to something like deVerifyFallback (with deFallbackReason being the general signal) would help future readers, non-blocking.

Review by Rames D Jusso

Comment thread packages/cli/src/commands/render.ts Outdated
* simply don't offer the trial when it can't be — batch concurrency is an
* explicit opt-in, not the common case.
*/
disableDeParallelRouterTrial?: boolean;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Trial safety is opt-out, and only from CLI batch code. renderLocal is exported from this file, so anything else in the codebase (or an external SDK consumer) that imports and drives it — e.g. a future studio-server render path, a scripted test harness, or a distributed-plan runner — inherits the trial and the process-wide env-var / module-level latch state without knowing to pass disableDeParallelRouterTrial: true. The doc-comment names --batch-concurrency N>=2 as the specific case, but the underlying issue is broader: any concurrent invocation of renderLocal in the same process races on deParallelRouterTrialManagedByUs / deParallelRouterTrialFiredThisProcess / process.env.HF_DE_PARALLEL_ROUTER.

Safer polarity: default the trial off for programmatic callers and add enableDeParallelRouterTrial?: boolean that the CLI top-level render command explicitly opts into (batch code stays off, current tests unchanged). Non-blocking — the CLI-only assumption is a reasonable scoping call — but the doc-comment could name the assumption explicitly ("only the top-level CLI render command should ever leave this false") so future callers hit a clear signpost.

Review by Rames D Jusso

…s (review)

Two non-blocking review notes from Rames, both addressed:

1. Trial polarity inverted to OPT-IN: disableDeParallelRouterTrial →
   enableDeParallelRouterTrial. renderLocal is exported, so any programmatic
   consumer (future studio-server path, test harness, distributed runner)
   previously inherited the trial and its process-wide env-var/module-latch
   state without knowing to disable it — and concurrent invocation races
   that state. Now only the CLI's own sequential call sites opt in (the
   single top-level render, and batch at concurrency 1); everyone else gets
   no trial by default. The doc comment names the sequential-invocation
   assumption explicitly.

2. deSelfVerifyFallback semantic narrowing documented at both declarations
   (RenderCaptureObservability + RenderPerfSummary.drawElement): since the
   pinned-fallback retry was widened, the flag means verify-triggered
   SPECIFICALLY — OOM/capture_error fallbacks report false with
   deFallbackReason carrying the reason. Dashboards keyed on
   de_self_verify_fallback=true as "any fallback fired" must migrate to
   de_fallback_reason IS NOT NULL (also called out in the PR body for the
   observability rebuild to pick up).

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

@miga-heygen miga-heygen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Re-review — #2115

Checked the delta since my last review (3 new commits). The changes are minor — the core architecture is unchanged. The new commit b02703b7 appears to be doc/polarity tweaks. No new SSOT concerns.

Previous verdict stands: no blockers, ship it.

— Miga

@james-russo-rames-d-jusso james-russo-rames-d-jusso left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Reviewed delta 6172d79db02703b7.

Both R1 items landed cleanly, and the resolution shapes are exactly the ones I'd have picked — appreciated that you took the polarity flip on the trial flag rather than the "add another guard on the concurrent path" workaround.

🟠 deSelfVerifyFallback semantic narrowing — resolved via docs + downstream migration hint. observability.ts:44-51 and renderOrchestrator.ts:406-412 now explicitly state that deSelfVerifyFallback narrowed to "verify-triggered" only, and name the compensating "any-fallback" signal (de_fallback_reason IS NOT NULL) that dashboard consumers should migrate to. Not renaming the field is the right call — a rename is its own migration hop through PostHog + any BI reports — so a doc-level pointer + explicit new-signal name is the cleanest bridge. Coordination note if it's useful: worth a heads-up in the HF observability rebuild thread (Miguel's #1231/#1233/#1248 lineage) so anyone tuning fallback dashboards there knows the two signals now split.

🟡 Trial-flag polarity flip — resolved as OPT-IN. render.ts:1068enableDeParallelRouterTrial?: boolean default OFF. Both CLI call sites migrated: single top-level render sets enableDeParallelRouterTrial: true, batch path sets batchConcurrency <= 1 (sequential opts in, concurrent leaves unset). maybeEnableDeParallelRouterTrial short-circuits when !enabled at line 1752. Doc comment fully rewritten and (nice touch) explicitly names studio-server / test harnesses / distributed runners as the "programmatic consumers who get no trial by default" — exactly the class of caller the R1 concern was about. Test at render.test.ts:755-766 inverted correctly — the const { enableDeParallelRouterTrial: _omitted, ...programmaticOptions } destructure is a clean way to simulate a programmatic caller in the fixture.

Ship it. LGTM from my side — leaving as a comment.

Review by Rames D Jusso

…ilure-telemetry

# Conflicts:
#	packages/cli/src/telemetry/config.ts

@miguel-heygen miguel-heygen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Final approval pass at current head 392dd410a582e74622cdacb5cfd1d7a67878defc.

Verified the current diff and the b02703b7 follow-up: trial routing is opt-in at intended CLI call sites, and deSelfVerifyFallback documentation points consumers to de_fallback_reason IS NOT NULL. No new functional drift found. All required CI checks are green. Remaining review threads are non-blocking documentation/coordination notes only.

@vanceingalls vanceingalls merged commit 469cfb3 into main Jul 11, 2026
47 checks passed
@vanceingalls vanceingalls deleted the de-parallel-router-failure-telemetry branch July 11, 2026 01:24

@jrusso1020 jrusso1020 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

APPROVE ✅

CI is now fully green on the merge head 392dd410a (44 checks pass, 0 fail — including all 8 regression shards, Windows render/tests, and CodeQL). That was the specific thing I was holding for: a render-path change on a fresh merge-of-main needs the visual-regression shards to clear on the actual merge head, and they did.

The R1/R2 review is thorough (Miga SSOT + tai/pr-review + Rames-D) with all non-blockers addressed: opt-in trial polarity (enableDeParallelRouterTrial, default OFF, programmatic consumers get no trial) and the deSelfVerifyFallback semantic narrowing documented with the de_fallback_reason IS NOT NULL migration hint. Head is the reviewed merge commit, no post-review push.

Approving per Vance's request. — Rames Jusso

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.

5 participants