Files
hyperframes/.fallowrc.jsonc
T
James RussoandJerrai 01a10cdc53 fix(producer): serve /health from a worker_thread so probes survive main-thread stalls (#1733)
* fix(producer): serve /health from a worker_thread so probes survive main-thread stalls

Adds an off-main-thread /health endpoint that listens on its own port
(default 9848, env PRODUCER_HEALTH_PORT). The endpoint binds inside a
Node worker_thread with a minimal node:http server — separate event loop,
separate isolate — so probe responses don't depend on whatever the
producer's main thread is doing.

Why now
-------

Today's hyperframes-producer crashloop traced to an infinite GSAP
timeline -> distributed planner trying to enumerate ~300,000,000,000
frames -> sidecar /health stops landing within k8s's 5s window ->
otherwise-healthy pods killed.

Miguel is shipping the root-cause fix at plan() time (impossible /
non-finite / sentinel durations get rejected before chunk planning).
That removes today's wedge.

This change is defense-in-depth for the kill mechanism. Even with the
plan() guard, future wedge classes can stall the main event loop for
seconds at a time: large synchronous file I/O (see the companion
fileServer streaming PR), GC pauses on long-running renders, tight
loops in user-authored GSAP / Three.js / canvas code, future
activity / pool changes whose runtime cost we haven't yet characterized.

Probe responsiveness should reflect process liveness, not main-thread
event-loop responsiveness. If the entire Node process is dead the OS
tears down both threads' sockets simultaneously and k8s correctly kills
the pod. Anything short of that and the worker thread's listener keeps
answering.

Backwards-compatible: the main-thread /health on PRODUCER_PORT (9847)
keeps working exactly as before. The k8s sidecar probe config in
heygen-com/app can migrate to the worker port at its own pace. A
companion heygen-com/app PR in this batch raises the probe timeout
from 5s -> 30s as a last-resort backstop.

TODO: link Miguel's upstream plan() duration guard PR once known.

Test: healthWorker.test.ts (vitest) — 3 tests pass locally, including
the load-bearing one: stays responsive while the main thread is blocked
on a 500ms sync busy-spin.

— Jerrai

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(producer): tighten healthWorker startup race + shutdown semantics

Addresses Miga's review on #1733.

- server.ts: store the worker as a Promise<HealthWorkerHandle | null>
  instead of mutating a `let` from inside `.then`. A SIGTERM landing
  before the `.then` callback fired would previously see `healthWorker
  === null` and skip cleanup. shutdown() now `await`s the promise with
  a bounded 1.5s timeout so a hung-startup worker can't keep SIGTERM
  waiting (worker.terminate() from process exit still kills it).
- healthWorkerThread.ts: replace `process.exit()` inside the worker
  with `parentPort.close()` + natural event-loop drain. Node-version
  semantics for `process.exit()` from a worker have been historically
  inconsistent; the documented clean path is to close the channel and
  let the worker exit naturally. Also drops the redundant 2s force-exit
  on shutdown — the parent already owns the authoritative deadline via
  Promise.race + worker.terminate(), so the worker-side timer was
  belt-and-suspenders noise.

Co-Authored-By: Jerrai <noreply@anthropic.com>

— Jerrai

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-26 08:47:49 -07:00

304 lines
14 KiB
JSON

{
"$schema": "https://raw.githubusercontent.com/fallow-rs/fallow/main/config-schema.json",
"entry": [
"packages/producer/src/**/*.test.ts",
"packages/aws-lambda/src/**/*.test.ts",
"packages/gcp-cloud-run/src/**/*.test.ts",
"packages/producer/src/regression-harness.ts",
"packages/producer/src/regression-harness-distributed.test.ts",
"packages/producer/src/regression-harness-lambda-local.ts",
"packages/producer/src/transparency-test.ts",
"packages/producer/src/parity-harness.ts",
"packages/producer/src/parity-fixtures.ts",
"packages/producer/src/perf-gate.ts",
"packages/producer/src/runtime-conformance.ts",
"packages/producer/src/benchmark.ts",
"packages/producer/scripts/generate-font-data.ts",
"packages/cli/scripts/generate-font-data.ts",
"packages/engine/scripts/test-fitTextFontSize-browser.ts",
"packages/aws-lambda/scripts/*.ts",
// Built as standalone IIFE for the browser-side sandbox runtime;
// referenced by file path (not import) in build-hyperframes-runtime-artifact.ts.
"packages/core/src/runtime/entry.ts",
// In-page audit scripts read as raw strings and injected via
// page.addScriptTag (see layout.ts / validate.ts) — referenced by file
// path, never imported, so they have no import-graph referrer.
"packages/cli/src/commands/layout-audit.browser.js",
"packages/cli/src/commands/contrast-audit.browser.js",
"packages/cli/src/commands/motion-sample.browser.js",
// Worker entry points loaded dynamically by their *Pool.ts companions.
"packages/producer/src/services/pngDecodeBlitWorker.ts",
"packages/producer/src/services/shaderTransitionWorker.ts",
// Off-main-thread /health endpoint, spawned by path from healthWorker.ts.
"packages/producer/src/services/healthWorkerThread.ts",
// Test fixture worker, spawned by path via the pools' workerEntryPath
// option from the crash-recovery tests; has no import-graph referrer.
"packages/producer/src/services/__fixtures__/crashOnMessageWorker.mjs",
"scripts/*.{ts,mjs,js}",
"scripts/*/run.mjs",
// Keyframe UI components — wired dynamically via EaseCurveSection/MotionPanel.
"packages/studio/src/components/editor/KeyframeDiamond.tsx",
"packages/studio/src/components/editor/SpringEaseEditor.tsx",
// NLE notice — rendered conditionally via NLELayout when timeline is first shown.
"packages/studio/src/components/nle/TimelineEditorNotice.tsx",
// Zoom hook extracted for downstream razor-blade PRs (#1330, #1331).
"packages/studio/src/player/components/useTimelineZoom.ts",
// Cached O(1) GSAP target lookup, replacing O(n²) inline checks.
// Consumers migrate in a follow-up once useDomGeometryCommits adopts it.
"packages/studio/src/hooks/gsapTargetCache.ts",
// Preview helper consumed dynamically from the studio iframe bridge.
"packages/studio/src/hooks/gsapRuntimePreview.ts",
],
"ignorePatterns": [
"docs/**",
"packages/producer/tests/**",
"packages/player/tests/**",
"packages/engine/tests/**",
"skills/**/test-corpus/**",
"skills/**/scripts/**",
// Agent-invoked motion-graphics tools co-located with their docs (run via
// `node <path>` per grounding/PROTOCOL.md / categories/maps/module.md
// prose), not import-graph reachable.
"skills/motion-graphics/grounding/**",
"skills/motion-graphics/categories/**",
// Agent-invoked reference materials (template + motion-primitive HTML, catalogs),
// forked by path by the frame-worker per SKILL.md prose, not import-graph reachable.
"skills/music-to-video/references/**",
// Bundled @font-face data (read at runtime via fs.readFileSync, invisible
// to the import graph) + its manual rebuild tool.
"skills/**/fonts/**",
// Golden snapshot files: data consumed by toMatchFileSnapshot, not importable modules.
"packages/**/__goldens__/**",
"registry/**",
"examples/**",
"packages/sdk/examples/**",
".github/workflows/fixtures/**",
// Auto-generated TS client for the HeyGen cloud API. Regenerated by
// experiment-framework/scripts/generate_hyperframes_cli_client.py via
// the sync-hyperframes-codegen.yml workflow; complexity/dead-code
// findings on this file are not actionable from this repo.
"packages/cli/src/cloud/_gen/**",
],
"ignoreExports": [
// CLI command files: every command exports a const `examples` per the
// convention documented in CLAUDE.md. This is a namespace barrel, not a
// collision.
{ "file": "packages/cli/src/commands/*.ts", "exports": ["examples"] },
// Independent ML model managers each declare their own DEFAULT_MODEL /
// MODELS_DIR / ensureModel for their model namespace.
{
"file": "packages/cli/src/{background-removal,tts,whisper}/manager.ts",
"exports": ["DEFAULT_MODEL", "MODELS_DIR", "ensureModel"],
},
// `isPathInside` is documented as exported-for-tests only in fileServer.ts;
// it has different semantics (symlink resolution) from utils/paths.ts.
{
"file": "packages/producer/src/services/fileServer.ts",
"exports": ["isPathInside"],
},
// Studio telemetry: consumed by useRenderQueue.ts / StudioFeedbackBar.tsx
// (deep relative imports) but fallow's static analyzer doesn't trace
// them. Same path-resolution quirk — trackStudioSessionStart from the
// same file resolves fine.
{
"file": "packages/studio/src/telemetry/events.ts",
"exports": ["trackStudioRenderStart", "trackStudioFeedback"],
},
// domEditingLayers: these exports are consumed via the browser iframe
// runtime context (not traceable by static import analysis from the
// studio entry point) or re-exported through the domEditing barrel but
// have no downstream consumers yet.
{
"file": "packages/studio/src/components/editor/domEditingLayers.ts",
"exports": [
"isEditableTextLeaf",
"collectDomEditTextFields",
"buildElementLabel",
"refreshDomEditSelection",
],
},
// domEditing barrel: re-exports consumed throughout the studio but
// fallow's static analyzer can't trace re-exports through barrel files.
{
"file": "packages/studio/src/components/editor/domEditing.ts",
"exports": ["*"],
},
// Exported for render.test.ts (exported-for-tests pattern).
{
"file": "packages/cli/src/commands/render.ts",
"exports": ["resolveBrowserGpuForCli", "renderLocal"],
},
// captureCost.ts: constants and helpers consumed by the runCaptureCalibration
// orchestration function and tests, but the entry-point graph doesn't
// reach them because the orchestrator's caller resolves them dynamically.
{
"file": "packages/producer/src/services/render/captureCost.ts",
"exports": [
"CAPTURE_CALIBRATION_TARGET_MS",
"MAX_MEASURED_CAPTURE_COST_MULTIPLIER",
"CAPTURE_CALIBRATION_PROTOCOL_TIMEOUT_MS",
"measureCaptureCostFromSession",
"logCaptureCalibrationResult",
"createFailedCaptureCalibrationEstimate",
],
},
// gsapParser.ts is a public-API barrel that re-exports constants, types,
// and utilities from gsapConstants, gsapSerialize, and springEase. The
// re-exports are intentional public API consumed by callers outside the
// changed-file set (e.g. studio, aws-lambda) and therefore appear unused
// to fallow's static analysis of the PR diff.
{
"file": "packages/core/src/parsers/gsapParser.ts",
"exports": [
"PROPERTY_GROUPS",
"classifyPropertyGroup",
"classifyTweenPropertyGroup",
"SPRING_PRESETS",
"generateSpringEaseData",
"GsapMethod",
"GsapKeyframesData",
"GsapKeyframeFormat",
"PropertyGroupName",
"SpringPreset",
],
},
// Shared test helpers consumed by gsapParser.test.ts (same file,
// fallow doesn't trace intra-file test consumption).
{
"file": "packages/core/src/parsers/gsapParser.test-helpers.ts",
"exports": [
"expectKeyframe",
"expectKeyframesFormat",
"convertAndReparse",
"parseSplitAndAssert",
],
},
// Shared timeline components extracted for downstream PRs in the
// razor-blade stack (#1330, #1331). Consumers live on those branches.
{
"file": "packages/studio/src/player/components/timelineCallbacks.ts",
"exports": ["*"],
},
// gsapTargetCache: cached O(1) GSAP target lookup, consumed by
// useDomEditCommits and intended to replace the local copy in
// useDomGeometryCommits once callers migrate.
{
"file": "packages/studio/src/hooks/gsapTargetCache.ts",
"exports": ["isElementGsapTargeted"],
},
// Re-exports from useDomEditCommits: barrel-style re-exports
// consumed by downstream studio code.
{
"file": "packages/studio/src/hooks/useDomEditCommits.ts",
"exports": ["GSAP_CSS_FALLBACK_BLOCKED_MESSAGE", "PersistDomEditOperations"],
},
{
"file": "packages/studio/src/utils/timelineElementSplit.ts",
"exports": ["buildPatchTarget", "readFileContent"],
},
],
"ignoreDependencies": [
// Runtime/dynamic deps not visible to static analysis: tsup `external`,
// dynamic require() resolution, peer/static-file consumption in tests,
// and bun-hoisted workspace devDeps (e.g. happy-dom in root package.json
// resolves for every workspace, so workspaces don't redeclare it).
// Required by @puppeteer/browsers and puppeteer-core at runtime; listed
// as a direct dep to guarantee installation even when transitive
// resolution fails (corrupted cache, dedup edge cases).
"debug",
"puppeteer",
"puppeteer-core",
"esbuild",
"giget",
"gsap",
"happy-dom",
"ffmpeg-static",
"ffprobe-static",
"@hyperframes/core",
"@hyperframes/studio",
"@hyperframes/producer",
"@fontsource/archivo-black",
"@fontsource/eb-garamond",
"@fontsource/ibm-plex-mono",
"@fontsource/inter",
"@fontsource/jetbrains-mono",
"@fontsource/league-gothic",
"@fontsource/montserrat",
"@fontsource/nunito",
"@fontsource/oswald",
"@fontsource/outfit",
"@fontsource/space-mono",
"@fontsource/lato",
"@fontsource/noto-sans-jp",
"@fontsource/open-sans",
"@fontsource/playfair-display",
"@fontsource/poppins",
"@fontsource/roboto",
"@fontsource/source-code-pro",
],
"duplicates": {
// Raise from the default 5 to 6 lines so trivially short Hono route-handler
// preambles (resolveProject + 404 + body-parse) are below the threshold.
// The three 5-line groups in files.ts / render.ts are structural boilerplate
// that naturally converges and is unlikely to diverge; extraction would
// require intrusive middleware changes beyond this PR's scope.
"minLines": 6,
"ignore": [
// slideshowPanelHelpers.ts: setSlideNotes/addFragment/addHotspot share an
// intentional parallel shape (signature + mapSlidesIn → exists-check →
// map/append); the per-slide mutation differs, so a shared abstraction
// would obscure more than it dedupes.
"packages/studio/src/components/panels/slideshowPanelHelpers.ts",
// SlideshowPanel.test.ts: parallel arrange/act/assert test cases — collapsing
// them would hurt readability of what each case verifies.
"packages/studio/src/components/panels/SlideshowPanel.test.ts",
// hyperframes-player.test.ts: parallel arrange/act/assert test cases verifying
// distinct behaviors (same-origin vs realm media, audio-locked permutations,
// seek bridge variants). Each case is self-contained for readability;
// extracting the iframe / mock-audio setup helpers would over-couple
// unrelated scenarios under a shared fixture.
"packages/player/src/hyperframes-player.test.ts",
// present.ts mirrors play.ts's server startup + console-output block. The
// shared low-level pieces (resolve*/injectRuntime/listenOnFreePort) are in
// utils/compositionServer.ts; the remaining clone is per-command logging text
// (different labels/help lines) — extracting it would over-abstract.
"packages/cli/src/commands/present.ts",
],
},
"health": {
// executeGsapMutation (introduced by Phase 3b / acorn-parser stack, already
// merged to origin/main via #1338) has CRITICAL cyclomatic complexity (58)
// that pre-dates this PR's scope. Excluding files.ts from health analysis
// avoids the inherited-fingerprint line-shift problem that suppression
// comments would cause (any inserted line shifts subsequent function line
// numbers, breaking fallow's inherited-detection fingerprint).
//
// useGsapTweenCache.ts: pre-existing large React-effect hooks (the populate
// and runtime-scan effects, the per-element animations memo) whose
// complexity pre-dates the computed-timeline work. Exempted at file level
// for the same reason as files.ts rather than refactored as scope creep.
//
// gsapParser.ts: the recast/babel GSAP writer is a 2500-line legacy parser
// restored as the default server writer by WS-3.F rework (acorn is now
// flag-gated behind STUDIO_SDK_CUTOVER_ENABLED). Its complexity pre-dates
// this PR and was present on all ancestor branches; the file-level exemption
// avoids the line-shift fingerprint problem for inherited findings.
"ignore": [
"packages/core/src/studio-api/routes/files.ts",
"packages/core/src/parsers/gsapParser.ts",
// SlideshowPanel.tsx: top-level editor panel that wires several independent
// sections (slides/inspector/branches/hotspot). Its cyclomatic count comes
// from that fan-out; splitting it would scatter shared state without
// reducing real complexity. File-level exemption (not an inline comment)
// avoids the line-shift fingerprint problem noted above.
"packages/studio/src/components/panels/SlideshowPanel.tsx",
// play.ts / present.ts: CLI command entrypoints whose cyclomatic count is
// browser/arg validation + server wiring (same shape as preview.ts). The
// serving logic is factored into utils/compositionServer.ts; the remaining
// body is linear validation that reads clearly inline.
"packages/cli/src/commands/play.ts",
"packages/cli/src/commands/present.ts",
],
},
}