Generation Request Lifecycle & Param Plumbing
SkillSearchTraces the full life of a music generation request (UI form → Node job queue → engine LM → synth → SQLite song row) and every place params can silently drop, especially the LM echo sideband gotcha. Use when adding/plumbing a generation parameter, debugging "param set in UI but engine uses default", touching /api/generate, translateParams, aceClient, hot-step-server.cpp request parsing, the LM/synth request rebuild, or understanding task modes (text2music/cover/repaint/lego/extract/complete), the serial job queue, and stall/timeout watchdogs.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Generation Request Lifecycle & Param Plumbing skill
What this skill tells your AI
The instructions your AI receives, as published by scragnog/hot-step-cpp in .claude/skills/generation-request-flow/SKILL.md and read by ahel’s review.
This skill maps the end-to-end path of a generation request in HOT-Step CPP and catalogs every point where a parameter can be silently lost. Most historical bugs in this area are param-plumbing bugs: a value set in the UI never reaches the C++ sampler, and nothing errors.
Terms used below:
- DiT — the diffusion transformer that turns audio codes + text embeddings into audio latents (the "synth" phase). Pipeline is LM → DiT → VAE.
- LM — the language model phase that generates metadata (bpm, duration, key), a chain-of-thought (CoT) caption, lyrics, and
audio_codesfor the DiT. - aceReq — the Node-side request object (interface
AceRequest, server/src/services/aceClient.ts:38) sent to the C++ engine. Snake_case, produced from camelCase UI params bytranslateParams(). - Sideband — HOT-Step-only fields (solver, scheduler, guidance, adapter knobs, …) that travel alongside the upstream C++
AceRequeststruct in the same JSON body but are parsed separately intoServerFields(engine) because they don't exist in the upstream struct.
When to use this skill
- Adding a new generation parameter (UI knob → engine behavior).
- Debugging: a param is set in the UI but the engine logs/behavior show the default.
- Modifying
server/src/routes/generate.ts,translateParams.ts,aceClient.ts, orengine/tools/hot-step-server.cpprequest handling. - Understanding generation modes (
text2music,cover,repaint,lego, …) or the job queue/watchdogs.
Golden rules
- NEVER rebuild the post-LM synth request by copying the LM echo or by whitelisting fields to carry across. The engine's
/lmresponse only contains fields in the C++AceRequeststruct — every sideband field vanishes. Always rebuild as{ ...aceReq, <the 7 LM output fields> }. WHY: every field-copy whitelist ever written here went stale and silently dropped params; commits8ea519band168dcb5fixed real bugs of exactly this class. Full detail in the next section. - When adding a float field to engine JSON parsing, use the int-safe
get_numlambda, not bareyyjson_get_real(). WHY:yyjson_get_real()returns 0 for JSON integers, and JavaScript serializes2.0as2— so a whole-number value silently becomes0.0. See hot-step-server.cpp:700-705. - C++ changes rebuild ONLY via
.\dev-rebuild.batat repo root — neverengine\build.cmddirectly, under any circumstances (you cannot reliably tell whether the app is running; Node auto-respawns ace-server on crash → infinite respawn + file-lock loop). Nevercmake --build . --clean-first(20+ min CUDA recompile). - Type-check TypeScript with
npx tsc --noEmit; do notnpm run buildduring dev. - After any upstream sync, run
powershell -File engine\verify-hooks.ps1. WHY:pipeline-synth-ops.cpplosing itshot-step-sampler.hinclude is SILENT — everything compiles but all solvers/schedulers/guidance/sideband go dead (they readg_hotstep_params, which nothing consumes anymore). - Git: all work on
master, stage explicit paths (nevergit add -A), push only with explicit user approval. Any pushedv*tag triggers a full multi-platform CI release build. - Params consumed as GPU tensors inside
hot-step-sampler.h(masks, encoder states, constants) must be re-uploaded EVERY step — the GGML scheduler aliases input tensor buffers as scratch space; uploading once leaves them garbage from step 2 onward (hot-step-sampler.h:591-601, re-upload sites :936, :1091). Never hoist an upload out of the step loop as an "optimization". - Never delete generated audio artifacts (
server/data/audio/*, test outputs) based on your own judgment of quality — the user verifies results by ear. - Node 18–22 LTS only — Node 24+ breaks dependencies (
enginesfield enforces<24).
THE LM ECHO SIDEBAND GOTCHA (read this before touching generate.ts)
Ground truth from the departing lead engineer, verified line-by-line in code:
Server-side params (ServerFields) do not survive the /lm round trip — the engine echoes back only what it knows. When building the synth request after the LM step, rebuild it from the original aceReq plus the LM-returned fields ONLY. Never maintain a whitelist of fields to copy across — every whitelist has gone stale and silently dropped params. Commits 8ea519b and 168dcb5 fixed bugs of exactly this class.
Mechanics of why:
/lm's result body isrequest_to_json()serialized from the C++AceRequeststruct (hot-step-server.cpp:869, struct at engine/src/request.h:29).- Fields that exist only in
ServerFields(hot-step-server.cpp:557-604) —vae_model,emb_model,scheduler,guidance_mode,adapter_group_scales,adapter_mode,adapter_runtime_quant,rebase_source/rebase_beta,apg_*,stork_substeps,denoise_*,plugin_params,seed_strength,evict_lm,vae_chunk,batch_cfg, and more — are not in the C++ struct, so the LM echo cannot contain them. Copy the echo forward and they all silently drop.
The correct pattern — it exists in exactly two places, keep them identical:
// generate.ts:319-328 (fresh LM) and generate.ts:237-246 (LM-cache hit)
lmResults = lmResults.map(lmOut => ({
...aceReq, // CURRENT request = every sideband field survives
audio_codes: lmOut.audio_codes, // then ONLY the 7 LM-generated fields:
caption: lmOut.caption,
lyrics: lmOut.lyrics,
bpm: lmOut.bpm,
duration: lmOut.duration,
keyscale: lmOut.keyscale,
timesignature: lmOut.timesignature,
}));
The only whitelist allowed is the LM-output list (those 7 fields). The base must always be a spread of the live aceReq. If you add a new server-side param you do not touch this rebuild code — that is the whole point of the pattern.
The pipeline at a glance
UI form (CreatePanel + Zustand globalParamsStore)
→ GenerationParams JSON, camelCase [ui/src/types.ts:80]
→ POST /api/generate (Node :3001) [server/src/routes/generate.ts:1355]
→ in-memory job Map + strictly serial queue [generate.ts:44 statuses]
→ translateParams(): camelCase → snake_case [server/src/services/generation/translateParams.ts:11]
→ POST http://:8085/lm (unless skipped) [aceClient.ts:319; engine handle_lm hot-step-server.cpp:888]
→ REBUILD synth req: {...aceReq, 7 LM fields} [generate.ts:319-328] ← THE SIDEBAND GOTCHA
→ POST http://:8085/synth (JSON or multipart) [aceClient.ts:332 / :347; engine handle_synth :1442]
→ engine: parse_server_fields() + request_parse_json() on the SAME body [hot-step-server.cpp:606]
→ synth_worker: ServerFields → g_hotstep_params global [hot-step-server.cpp:949, copy at :1132-1194]
→ synth_batch_run: TextEnc → DiT (hot-step-sampler.h reads g_hotstep_params) → VAE → WAV/MP3
→ Node saves data/audio/<uuid>.wav|mp3 [generate.ts:840]
→ auto-trim → post-processing chain → INSERT INTO songs [generate.ts:1057-1094, :1152]
Engine port 8085 is set in server/src/config.ts:102 (ACESTEPCPP_PORT). Node job statuses: pending | lm_running | synth_running | saving | succeeded | failed | cancelled (generate.ts:44).
Procedure: adding a new generation parameter end-to-end
A new param must be threaded through five layers. Missing any one = silent default.
- UI state: add to
ui/src/stores/globalParamsStore.ts(persisted per-field to localStorage under anhs-*key) and include it ingetGlobalParams()(line 324). Watch the conditional gating there — many params are sent only when their mode is active (e.g.apgMomentumonly whenguidanceMode === 'apg', line 396). Add the field toGenerationParamsinui/src/types.ts:80. The visible controls for engine knobs live inui/src/components/global-bar/(GlobalParamBar.tsx,GenerationDropdown.tsx) — grep an existing knob's setter to find the right spot. - Node translate: map camelCase → snake_case in
server/src/services/generation/translateParams.ts. Use!== undefinedguards for fields where0/falseis meaningful — a truthy guard (if (params.foo)) silently drops legitimate zeros. - Node wire type: add the snake_case field to the
AceRequestinterface inserver/src/services/aceClient.ts:38(this interface is the Node-side canonical superset: C++ struct fields + sideband). - Engine parse: add to
ServerFields(hot-step-server.cpp:557) andparse_server_fields()(:606). For floats, use theget_numlambda pattern — Golden rule 2. Note:get_numis declared mid-function at :703 — add your parse AFTER that line (e.g. next to the denoise fields at :763), or inline theis_real ? get_real : get_intternary likerebase_betadoes (:663); a parse placed above :703 fails to compile (use before declaration). - Engine global: copy into
g_hotstep_paramsin the block at hot-step-server.cpp:1132-1194 (note: this copy happens beforeace_synth_load()because adapter merge reads group scales at merge time), and add the field toHotStepParamsinengine/src/hot-step-params.h. Consume it inhot-step-sampler.h(or wherever). - Do NOT touch the LM-echo rebuild in generate.ts — the
...aceReqspread carries your field automatically. - Rebuild:
.\dev-rebuild.bat(C++),npx tsc --noEmit(TS). Verify arrival — but beware the[DIAG]trap: the[DIAG] /synth body (first 300 chars)line (hot-step-server.cpp:1568) truncates at 300 chars, and the body starts with caption then lyrics, so with any realistic caption your sideband field will NOT appear in the visible prefix. A field absent from the DIAG line is NOT evidence it was dropped. Reliable checks: add a temporaryfprintfof your parsed value inparse_server_fields(or next to the[Server] HOT-Step params:summary at hot-step-server.cpp:1197), or log the fullaceReqserver-side beforesubmitSynth. For any new UI control, ask the user to visually confirm it — do not use the browser agent for visual verification (API checks are fine).
Key files
| Path | Role |
|---|---|
ui/src/stores/globalParamsStore.ts | Zustand store for all engine knobs; getGlobalParams() (:324) is the single assembly point with mode-conditional gating |
ui/src/types.ts:80 | GenerationParams — canonical camelCase UI shape (~120 fields, incl. post-processing-only fields that never reach the engine) |
ui/src/App.tsx:502 | handleGenerate — merges global params + Create-tab content, enqueues via enqueueSimpleGen |
ui/src/services/api.ts:148-153 | generateApi — POST /generate, status polling, cancel |
server/src/routes/generate.ts | The whole Node lifecycle: queue, LM phase, sideband rebuild, trigger re-injection, synth loop, watchdogs, DB insert |
server/src/services/generation/translateParams.ts | camelCase → AceRequest snake_case; pure function |
server/src/services/generation/lmCache.ts | LM output cache — stores only the 7 LM fields, keyed by LM-affecting params only (sha256/16, LRU max 20) |
server/src/services/aceClient.ts | Wire types + HTTP client to engine :8085; timeouts 15 s submit / 30 s poll / 300 s result (:17-19) |
engine/tools/hot-step-server.cpp | Engine HTTP server: handle_lm (:888), handle_synth (:1442), ServerFields (:557), parse_server_fields (:606), synth_worker (:949) |
engine/src/hot-step-params.h | HotStepParams sideband global (g_hotstep_params) read by hot-step-sampler.h inside the DiT; also AdapterGroupScales/AdapterSpec/AdapterSection |
engine/src/request.h:29 | Upstream C++ AceRequest struct — fields here DO survive the /lm echo |
server/src/config.ts:102 | Engine port (8085 default) |
Every place params can silently drop (checklist)
- The LM echo (see gotcha section above) — THE classic.
- yyjson int-vs-float trap — fields parsed with bare
yyjson_get_real()(e.g.apg_momentum:667,apg_norm_threshold:670,seed_strength:674,beat_stability,dcw_scaler,latent_shift,cfg_cutoff_ratio,cache_ratio) turn whole-number JSON values into0.0. Onlyadapter_group_scales,rebase_beta, and a few others use the int-safeget_numlambda (:703). - UI-side gating in
getGlobalParams()(globalParamsStore.ts:324-458) — params conditionallyundefinedbased on mode/toggles. A staleadvancedAdapters=falsesuppresses a persisted adapter stack (:334). If a param "never arrives," check the gate here first. - translateParams falsy guards —
if (params.bpm)/if (params.duration)/if (params.inferenceSteps)intentionally drop 0 ("LM fills it in"), but a new field copied with a truthy guard drops legitimate0/false. Use!== undefinedwhere 0 matters. - Trigger word destroyed by the LM's CoT caption —
translateParamsinjects it (:136-144), the LM's chain-of-thought caption replaces the whole caption, and generate.ts re-injects it (:366-403) only whentriggerPlacementANDloraPathare both set; otherwise it logs a[Trigger]WARNING and skips (:399-403). Debugging "adapter trigger missing from caption" starts here. - LM cache staleness — only the 7 LM fields are cached/restored, keyed by
computeLmCacheKey(lmCache.ts:23-46). If you add an LM-affecting param, it MUST go into that key or stale audio codes get served for new settings. DiT/adapter params are deliberately excluded from key and value. - Multipart array truncation —
submitSynthMultipartsends onlyrequest[0]if given an array (aceClient.ts:376). - Engine registry fallbacks — unknown
vae_model/emb_modelfall back to the default with only a stderr warning, not an error; unknownrebase_sourceis skipped with a warning. Unknown adapter name (after absolute-path fallback) DOES fail the job. - Duplicated fields —
dcw_*,latent_shift/latent_rescale,custom_timesteps,infer_methodexist in BOTH the C++AceRequeststruct (request.h) andServerFields. The server sampler reads theg_hotstep_params(ServerFields) copy; keep both parse paths in sync or CLI-tool vs server behavior diverges. Also note naming: the C++ struct hasvae(request.h:142, used by CLI//vae) while the server sideband usesvae_model. - Upstream sync hook loss — silent solver/guidance/sideband death; run
engine\verify-hooks.ps1. - LM-skip paths (Song Builder, cover tasks,
skipLm) bypass the LM entirely — anything depending on LM-filled metadata must handle 0/empty. WhenskipLmon non-cover: defaults filled (bpm 120, duration 120, 'C major', '4') at generate.ts:221-224.
Generation modes (code truth)
Engine task_type (request.h:109, docs in engine/docs/ARCHITECTURE.md): text2music (default), cover (FSQ roundtrip, free reinterpretation), cover-nofsq (clean latents, faithful remix), repaint (region regenerate via repainting_start/repainting_end; negative start = outpaint before), lego (new layered track; base model only), extract (stem isolation; base only), complete (fill mix around a stem; base only). LM modes: generate / inspire / format.
Discrepancy — code wins: ARCHITECTURE.md:360 says lego uses the LM, but Node's isCoverTask list ['cover','cover-nofsq','repaint','lego','extract'] (generate.ts:208) means the Node pipeline never runs the LM for lego. complete is absent from that list, so it would run the LM if submitted via /api/generate. That list is Node-side policy, not the engine's task registry.
Failure signatures
| Symptom | Cause / where to look |
|---|---|
| Param set in UI, engine behaves as default | Sideband dropped post-LM (gotcha), UI gate (checklist #3), or whole-number float → 0 (checklist #2). Do NOT trust the 300-char [DIAG] /synth body prefix as proof of absence (procedure step 7) — add a temporary parse-site fprintf instead |
| "Generation stalled — no progress for Ns" | 2-min no-progress watchdog (generate.ts:107,:128); engine wedged |
| "Generation timed out (N min limit)" | Wall-clock watchdog, generationTimeoutMinutes clamped [5,120], default 45 (generate.ts:139) |
| "Generation failed on ace-server" | Engine job status 2 — read logs\<session>\generations\gen_<uuid>_*.log, then ace_engine.log for [Server] Adapter not found / synth load failure |
| All solvers/schedulers behave like euler after an upstream sync | Lost hot-step-sampler.h hook in pipeline-synth-ops.cpp — SILENT; run engine\verify-hooks.ps1 |
| Trigger word absent from final caption | CoT caption replaced it and re-injection gate unmet — grep gen log for [Trigger] WARNING |
| Stale LM output despite changed DiT/adapter params | Correct behavior (LM cache keys only LM params); set cacheLmCodes: false to force a fresh LM run |
| 503 "Engine not ready" on submit | Engine bootstrap unfinished (generate.ts:1355 handler checks engineReady) |
| Song Builder tracks get quieter each append | wav16 peak-renormalization — fixed by forced wav32 for builder/PP (generate.ts:549-566) |
Institutional knowledge
- VALIDATED — LM echo sideband gotcha: described above; the rebuild pattern at generate.ts:237-246 and :319-328 is the fix for real shipped bugs (commits
8ea519b,168dcb5). - VALIDATED — yyjson int trap: documented in-code at hot-step-server.cpp:700-705; use
get_num. - VALIDATED — copy-before-load ordering:
g_hotstep_paramsmust be populated beforeace_synth_load()because adapter merge reads group scales at merge time (comment near hot-step-server.cpp:1128). - VALIDATED — zombie plumbing: regional self-attn isolation was reverted (
0f3bf6d→ee041e1, "broke musical continuity"), butadapter_section_isolationis still parsed (:656) and copied tog_hotstep_params(:1145) while nothing inengine/src/consumes it. It is a dead knob — don't mistake its plumbing for a working feature, and do NOT re-implement the consumer: per-section self-attn masking breaks musical continuity across section boundaries; any retry needs a cross-section-coherence design first. - VALIDATED — auto-shift fork behavior: when
shift == -1, this fork computes adaptive shift from duration + steps (hot-step-server.cpp:1244-1250). ARCHITECTURE.md's "shift 0 = auto" text describes upstream, not this fork. - VALIDATED — DiT batch clamp:
synth_batch_sizeexpands per-seed variants; total clamped to DiT max 9 (hot-step-server.cpp:964-973). Node avoids enginemultipart/mixedresults by submitting one track per/synthcall. - UNVALIDATED / HYPOTHESIS — basin re-base: the β·(S−T) nudge for cross-base LoKR adapters is plumbed end-to-end (globalParamsStore:381 → translateParams:126 → ServerFields:575 → g_hotstep_params:1168) but its effectiveness is unproven. Invariant (VALIDATED): the nudge applies to the FIRST adapter of a stack only (
dit.h:565-579—rb_src = (si == 0) ? rebase_source : nullptr); one nudge per stack, never per adapter. Any plumbing change that makes rebase per-adapter is a bug (issue #72).
Useful commands (PowerShell)
npx tsc --noEmit # type-check server/UI (never npm run build in dev)
.\dev-rebuild.bat # the ONLY sanctioned way to rebuild C++, always
powershell -File engine\verify-hooks.ps1 # after any upstream sync
Get-ChildItem logs | Sort-Object Name -Descending | Select-Object -First 1 # newest session log folder
Invoke-RestMethod http://localhost:3001/api/generate/queue # queue health
Invoke-RestMethod -Method Post http://localhost:3001/api/generate/reset-queue # unwedge the queue
Deeper reading
- reference.md (same folder) — full node-tier walkthrough, ServerFields field list, endpoints/watchdogs, LM cache internals, translateParams special cases.
- engine/docs/ARCHITECTURE.md — engine internals, task types, request JSON. Caveat: its request-JSON reference predates several sideband fields; the code (
aceClient.tsAceRequestinterface +ServerFields) is the authoritative schema. - docs/PLUGINS.md — Lua solver/scheduler/guidance plugins (how
plugin_paramsare consumed). docs/plans/— internal design docs (per-section masking, upstream sync workflow). Gitignored, local-only — may be absent on a fresh clone.- CLAUDE.md — build/git rules and log layout.
Signals
- GitHub stars
- 137
- Forks
- 20
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
generation-request-flow- Source
- github.com/scragnog/hot-step-cpp