Repository navigation
Conversation
… per-entry linear A resident for_each's projection divided the whole observed peak (dominated by the one-time pipeline load) by the entry count and multiplied back out, projecting a 12-entry batch at roughly five times its measured peak. With history at two or more distinct list lengths, base and marginal growth are now fit through the smallest and largest observed counts; with history at only one length, the measured peak is projected flat rather than scaled. Also drops the "held resident together" wording for text naming what was actually fit, and suppresses the projection entirely for a task-only workflow with no pipeline step to accumulate. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Independent read-only review of develop @ 09ce397 (issues #17-#367, git history since the web UI/MCP additions) and a phased plan built from it: security fixes, CI on develop + durable jobs, one prepare pipeline for run/validate/plan, audio value model, fitted estimator, server router split, explicit workflow-language forms, and loop process changes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…not the generic (image, device) shape get_task/list_tasks reported every image processor as a generic ControlNet preprocessor with accepts_kwargs: true, hiding real keyword arguments (e.g. recenter_crop's center_x, center_y, crop, width, height, fill). image_processor_target maps a processor name to its plain backing function when one exists, and describe_task introspects it directly; a detector-backed processor (controlnet_aux, transformers, dw_pose, sam) still falls back to the old generic shape. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…whole clip get_frame/get_first_frame/get_last_frame only ever need one frame, but reached it through fetch_video/load_video's full decode, which SIGKILL'd on large clips. VideoFileReference wraps a validated file path; realize_args routes a get_frame-family task's 'video' argument to it instead of the eager loader when the source is a file/URL reference, and get_frame seeks to the requested frame with PyAV's frames_at instead of decoding every frame. The existing past-the-end error wording is preserved. Adds tests/test_video_utils.py::TestVideoFileReference, including a monkeypatch proof that load_video is never called for these commands. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…own summary
get_frame's docstring gave get_task("get_frame") a real summary, but
get_first_frame/get_last_frame share its implementation and so shared
that same generic summary, with no mention of which end of the clip
each pulls from. _VIDEO_PROCESSOR_INFO entries can now carry a
'summary' override, which describe_task prefers over the shared
implementation's docstring.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… mounted download_output refusal export_job and list_gallery return only server-relative URLs, which are useless handed to a person who isn't a browser already pointed at the box; export_job's instructions also told an MCP-only agent to fetch its own zip even when the endpoint requires auth it cannot attach. And a mounted download_output with no destination silently wrote into the workspace root, where nothing could find or delete it later. - Add a DW_PUBLIC_URL env var / public_url setting. When configured, export_job and list_gallery additionally return absolute_url / absolute_zip_url built from it; unconfigured, those fields are omitted rather than guessed from request/forwarded headers. - export_job's `next` field, and the export paragraphs in the minimax-h3, ltx-2.5 and minimax-music3 plugin skills, now say: when auth_required is true, hand the URL to the person instead of fetching it, and keep using get_output_image/_audio/_frames for inline content. - download_output on a mounted endpoint now refuses an omitted destination with a message pointing at keep_output and the gallery URL, instead of defaulting into the workspace root. An explicit destination is unaffected. Breaking change: the download_output refusal turns a previously-succeeding mounted call (omitted destination) into an error for existing callers. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…claiming nothing clears VRAM minimax-music3 and ltx-2.5 SKILL.md step 4 said "Nothing over MCP clears it ... ask the operator to restart the worker," but clear_memory does clear resident VRAM (and drops the step cache) while the server is idle. Point the agent at clear_memory + a re-read of get_memory instead, keeping the don't-retry-into-a-failed-attempt reasoning. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… duration to list_gallery
Every gallery read that resolves a name through _output_file (metadata,
audio, frames, thumbnail, download, delete, keep, archive, and the static
/outputs/{name} route) rejected an "output:"-prefixed name with a
misleading "path does not exist", even though that is exactly how a
workflow argument references the same file. A shared _strip_output_prefix
helper strips one leading "output:" before the name is used for path
resolution, job lookup, or run-path parsing - containment is still
validate_path's, applied to the stripped remainder.
GET /api/gallery also gains an opt-in media=true that adds
duration_seconds to audio/video entries (via the same probe_media
get_gallery_metadata already uses), bounded to the page actually
returned so distinguishing two takes of one workflow no longer needs one
metadata call per candidate. Default listing is unchanged.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… a fault The watchdog's repeat report bumped its own last-event clock, so a caller reading get_job_events during a long healthy silence (H3's reference encode, block-cache gaps) saw fault-shaped warnings with a climbing event_count and mistook them for liveness or a stuck run. RunContext now tracks the last *real* progress event separately from the watchdog's own emits, and the phase_stall message and a new seconds_since_last_progress field report time since that last real event rather than resetting each time the watchdog itself speaks. get_job_events/wait_for_job docstrings and the minimax-h3 skill are updated to say phase_stall narrates silence rather than signaling a fault. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Three MCP tool call sites steered agents to the wrong fix: - validate_workflow, run_workflow and save_prompt rejected a JSON-encoded string for a dict-typed param with a raw pydantic dict_type error. They now widen the annotation to dict | str and run the value through a shared coerce_json_object() (moved from authoring.py's local _coerce_json_object, now in dw_mcp/client.py so authoring.py, diagnose.py and prompts.py all use one implementation), matching the coercion save_workflow already had from #247. A string that fails to parse is now reported as invalid JSON, not a type mismatch. - get_output_frames's _moment_to_index (dw/media_frames.py) gave an ambiguous "130.0 is past the end of a 141-frame clip" for an out-of-range bare number. The out-of-range message for a bare (seconds) value now says the clip's duration in seconds, spells out that bare numbers are seconds, and suggests the frame:N form with the caller's own numeral. - concat_videos/dissolve_videos's level_spread warning (warn_on_level_spread, dw/tasks/audio_utils.py) always told a caller to pass match_levels, even when a level difference between shots is intentional (e.g. a shot deliberately silent against a score hole). The message now names that possibility before recommending match_levels. kind="level_spread" and its other fields are unchanged so existing regression-suite matches on kind still hold. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Per #298 the mounted server shares one pin across all clients; a client whose pin was moved by another client's use_workspace/ create_workspace only found out by going looking for its outputs. run_workflow's queued (non-wait) answer now carries job["workspace"], matching what the wait_seconds>0 path already returns via _SLIM_KEYS. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The default mp3 deliverable normalized to -1 dBFS still clipped after encode (+0.56 dBFS measured), contradicting the engine's own audio_clipped warning, which prescribes -3. Every other audio-deliverable template already uses -3; bring music.json in line and update its description and the pinning test in tests/test_result.py. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… task argument is a warning, not a refusal A required task argument written as `variable:name`, where `name`'s declared default is null, was reported as "the step does not supply" it and refused by save_workflow and by validate_workflow with no arguments - even though the step does supply it, just not yet a value. task_signature_errors now compares the expanded definition against the author-written one: when a missing required key was written as `variable:name` and name resolves to null, the error carries a `variable` key and a message naming the variable instead of the generic #141 wording. Workflow.validation_errors downgrades that specific case to a warning when called with no arguments (save, or validate with no arguments), via the new null_variable_argument_warnings(), while keeping it a hard error whenever the run/validate call's own arguments leave the variable null. The schema's null description now explains the caveat. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…iables dict by variable name realize_args applied its image/video/type key-name conventions when realizing the variables dict, keying off the variable's own name rather than the argument it would end up filling - a variable named "image" fed into a step's "video" argument was pre-loaded as a PIL Image before substitution ever ran, and fetch_video then raised a raw "got <class 'PIL.Image.Image'>" naming neither the argument nor the step. Add apply_key_conventions to realize_args (default True, unchanged for every step-level call) and pass apply_key_conventions=False only at the variables-dict call site in workflow.py's _prepare_definition - explicit references (asset:/output:/constant:/prompt:/media_type objects) still resolve there, but a plain value now loads only once substituted into a real argument key. Any type mismatch that still reaches a loader now names the argument, the step, and what the value already was (_fetch_image_with_context/_fetch_video_with_context, _describe_value_source). Catalog sweep of workflows/** found no variable whose name and consuming argument key disagree on media type, so no template needed remediation. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ent's 2,048-char cut Claude Code truncates an MCP server's instructions and each tool description at 2,048 characters. The instructions (4,056) lost the run loop, acknowledged_cost and the reference prefixes; validate_workflow (2,803) lost its plan/estimate paragraph; list_workflows (2,129) its tail. Rewrite all three under the limit, put the plan first in validate_workflow, and pin every text with CLIENT_TEXT_LIMIT. Also drop issue numbers from agent-facing docstrings and skills, and correct minimax-music3's "you cannot listen" now that get_output_audio exists. ltx-2.5 keeps its DFR fps note, trimmed elsewhere to stay under the 12 KiB skill cap. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- h3-video-mux-headroom-warning-partial.md: fixes (1)-(4) shipped in bbe4adb and the suite wording landed via #323. The resolution and the still-deferred fix (2), with its undershoot evidence, move into the complete doc. - live-memory-during-run.md: design (a) shipped in fe824c9 (#273). The unshipped fallback (b) is recorded on #273. - todo.md: Tier 1 is empty. Open items are now `feature` issues (#374-#380, #244). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A review of the suite for tests that are not quality gates. Fixed rather than removed: - test_download_watch: stall threshold was 2x the emit interval, so scheduler jitter tripped it (failed 4/5 runs); now ~8x like production - test_text_sections: H3 prompt glob matched nothing since the templates moved; repointed, plus a guard that the sweep is non-empty - catalog sweeps in test_elision, test_component_type_errors, test_task_signature_errors, test_pair_audio_fit: anchored to the repo root so they don't collect nothing when run from elsewhere - test_workflow_trust: set_trust_workflows(True) could not fail under conftest's DW_TRUST_WORKFLOWS=1 - keep-as-asset: link test asserted only inside `if linked`; now required, and the copy fallback gets its own test - several can't-fail or tautological tests rewritten to assert real behaviour (wait_for_job cap, REPL commands, gather, cached pipeline arguments/seed, watermark fallback, format.ts mtime, and others) Removed: test_worker_with_simple_workflow (strict subset of the cache-hit test, ~16s), assertion-free REPL scripts, dead __main__ blocks, and verified duplicates/trivial tests across ~25 files. Suite: 5720 passed, 14 skipped, 107s (was 5772/17, 150s). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Policy: delete verified duplicates, keep prose pins on tool descriptions and parameter-forwarding tests, fix rather than delete tests that were flaky or asserted nothing. - MCP: per-tool pass-through and error-passthrough tests removed where TOOL_WIRING and test_mcp_client already cover the tool; added a client test for the streamed get_media_if error path they were the only cover for - server/result/elision/model_cache/etc: duplicates removed, unique assertions moved into the surviving test; prompt-reference sweep now also covers dw/workflows - getters and restated constants removed; vocabularies, schema closure and API fields (cost_basis, common_assets) kept - fixed: test_events watchdog timing (6x ratio, mutation-checked), integration tests now assert decoded outputs (multi-step fixture was eliding step 1), watermark/recenter/netinfo/host-memory/type-helper tests assert real effects Suite: 5636 passed, 14 skipped. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ts list get_iterations handed an 'inputs' list template back untouched, so "inputs": ["previous_result:step1"] reached the task as the literal string - while previous_result_reference_errors already validated the same reference as real, so nothing warned. A reference entry now expands to one iteration per result it names, and an object entry expands the way an 'arguments' template does; other entries pass through as before. The list is capped at MAX_ITERATIONS like the cartesian product. Found by the test-suite review: test_simple_dependency passed while its second step received the reference string. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…in skills #340: minimax-h3, minimax-music3 and series-episodes skills now teach ducking the *score* under a voice-over shot with gain_audio regions (one per voice-over shot, start_frame = running sum of preceding shots' num_frames, applied before the track is passed as `score`), rather than raising `world_gain`, which lifts narration and action sound together. Names the measured symptom: narration ~24dB over its own ambience, score 6-15dB over that. #354: get_output_audio's docstring, and the minimax-h3/series-episodes "Run and judge" sections, now point at the two-call transcription route (run_workflow(name="templates/transcribe-audio", ...) then get_output_text) instead of implying a new read tool. Verified live against dw.serve on both a plain wav and an H3 mp4 with a muxed soundtrack - both transcribed correctly with no code changes needed. minimax-h3/SKILL.md trimmed for the 12KB skill size cap while adding the ducking recipe and transcription pointer. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…mposing step's arguments observed_for_child was called with only (path, child_definition), so a composed child's observed-history rollup always answered the *default* bucket's figure regardless of what the composing step actually passed for a declared scalar cost_driver - bypassing the #267/#341 shifted-driver check entirely whenever any observed history existed. Thread the composing step's own arguments through dw/plan.py's estimate() into dw/server/app.py's observed_for_child, matching the pattern the top-level observed callback already uses, so the lookup buckets against the value the step runs with and correctly falls through to the catalog + _scalar_driver_shifted unpriced check when no history exists for that bucket. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…et reconstruction blind window The prior fix (aa67bf2) only emitted download_progress on detected growth in the hub cache's blob directory, which goes blind during an xet-backed transfer: reconstruction against the local CAS cache can leave the on-disk size unchanged for 20+ seconds at a stretch even while genuinely still running, reproducing the exact phase_stall silence #343 reported. No on-disk location (blob dir, HF_XET_CACHE, the tracked .incomplete file itself) closes that gap by polling alone. DownloadWatch._run() now emits on a fixed cadence regardless of growth, reporting bytes_per_second=0 honestly during a quiet interval instead of staying silent - a heartbeat, not a rate guess. This still only watches; it does not intercept or otherwise change what from_pretrained fetches. Traded away deliberately: a watched download can no longer trip phase_stall for as long as DownloadWatch's thread is alive, including in the case of a true from_pretrained hang with no eventual timeout. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
New grade task command (exposure, contrast, saturation, temperature, tint), CPU-only via numpy/PIL, per Don's narrow v1 disposition. Video is graded per frame through the existing _per_frame helper, which carries audio and fps through untouched. contrast/saturation are declared NON_NEGATIVE in task_domains.py so validate_workflow refuses a negative value before a run; exposure/temperature/tint have no natural non-negative-only domain and are left to the command itself, per that module's own rule that only undebatable domains are declared there. temperature/tint use a documented linear scale, +-1.0 shifting the R/B or G channel by up to 15% of the channel range. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
fix(engine): #349 - add a CPU grade task for images and video Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… to a not-found error DwApiError now carries the response status_code, and delete_workspace's unacknowledged probe call only appends the "call again with acknowledged_cost=True" sentence when the server answered 409 (needs acknowledgement). A 404 (no such workspace) or 400 (the default workspace) can never be fixed by acknowledging, so those errors now pass through unmodified. Checked delete_model/download_model/update_diffusers/enhance_prompt (models.py, prompts.py) - each raises its refusal locally before any server call, with no try/append pattern, so none share this bug; delete_output/delete_asset/ delete_workflow/delete_prompt take no acknowledged_cost at all. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… curated cost, so it's reported as cold not warm A workflow with no pipeline/pipeline_reference/workflow step never logs a "phase": "loading" event, so every run was classified had_load=False and bucketed as warm - cold_minutes never appeared, and dw/plan.py's _observed() only ever quotes cold_minutes for basis: "observed". That silently fell back to basis: "catalog" with no explanation, for any task-only template (e.g. templates/dissolve-between-shots) no matter how much history it had. _is_task_only() mirrors dw/plan.py's existing _has_seedable_step check; observed_for() now treats a task-only workflow's whole duration as cold, since it has no model-load phase to separate out. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
get_gallery_metadata's 'next' hint fired the same Music 3 audio_duration-ceiling and mp3-overshoot text for any audio or video output, so a video cut carried guidance about an audio duration ceiling that never applied to it. Video keeps the generic peak/mean level guidance; the Music 3-specific text is audio-only. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ates restore-decompression, restore-deblur and reference-sheet all configured a duration_head component on LTX2InContextPipeline, which never registers one - diffusers refused it 3s into the loading phase, after validate_workflow had reported valid:true and the plan had already quoted the checkpoint and LoRA download. generative-upscale's IC-LoRA step already had the correct component list and needed no change. Also adds component_name_errors (dw/introspection.py), checked in validation_errors beside #345's component_type_errors: a step's configuration.components naming something its component_type's __init__ does not register is now a validate-time error rather than a run-time one, so this class of bug can't pass validation again. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…eo's dict/list handling
validate_workflow's own hint tells a caller to pass a still to a `video`
argument as {"media_type": "image", "location": ...}, but fetch_video
only checked for this convention on a plain dict, not per-item inside a
video-keyed list - so the hint validated clean and then failed at run
time on the still's extension. fetch_video now checks is_media_reference
up front, which also covers the recursive per-list-item calls.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
get_frame/get_first_frame/get_last_frame and the assessment probes share _frames_of to unwrap a video argument into frames, but it raised on a bare PIL.Image - the same media_type-loaded still fetch_video now hands back for #443's fix. _frames_of treats a bare Image as a one-frame video, the same accommodation loop_frames already made for itself with _is_frame, so every _frames_of consumer benefits without a per-task fix. Audited every other task/command that takes a `video` argument per the triage note: loop_frames was already fixed by #347; interpolate_frames (needs >=2 frames), concat_videos/dissolve_videos (plural `videos` key, not the `video`/`*_video` convention) and pair_audio (needs a real duration/fps to fit against audio) are out of scope by their own semantics. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
LTX2ReferenceCondition.frames (the reference-sheet template's only consumer of loop_frames) hands a raw ndarray straight to diffusers' VaeImageProcessor.preprocess, which normalizes it as 2*x-1 with no /255 rescaling for a raw ndarray input - only a PIL-list input gets that scaling. loop_frames returned frames_as_array's uint8 [0,255] output unchanged, so the encoder saw values up to ~509 instead of [-1,1]: garbage-range VAE input, producing the reported blocky/corrupted video. frames_as_array's own uint8 contract is left untouched, since dissolve_videos.py depends on it staying [0,255]. loop_frames now converts its own return value to float32 [0,1] - diffusers' own np frame convention, already documented in frames_as_array's docstring and already handled correctly by result.py's frames_for_encoding if this step's output were ever saved as a real video. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…was actually provisioned The template's default asset:reference_sheet.png was never resolvable on lem - no such file existed in any asset library, so the template could not be run on its own stock defaults for a clean positive control. A stock composite reference sheet was generated and kept into common/assets/ via keep_output(shared=true); the asset library names files by their real extension, and the render came back as a .jpg, so the default now points at asset:reference_sheet.jpg to match what is actually on the box. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fixes #444: loop_frames now returns diffusers' [0,1] float32 np convention instead of raw uint8, fixing corrupted reference-conditioned video from templates/ltx2/reference-sheet; and the template's default reference_sheet asset now points at a stock composite actually provisioned in common/assets/. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
get_output_image and get_output_text (dw_mcp/media.py) both request
GET /outputs/{name}, which only stripped an output: prefix and served
the name literally through StaticFiles - an asset: name 404'd with a
bare "Not Found" instead of resolving the way every /api/gallery/{name}
route (metadata, /frames, /audio, /assess) already does. The route now
checks is_asset_reference(name) and serves through _asset_file's
resolved path, keeping ETag/Range support via StaticFiles bound to the
asset's own directory.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The #445 asset branch of /outputs raised _asset_file's 404, whose detail lists every asset root by absolute path. /outputs is outside the token gate (the middleware covers /api/ and /mcp), so any client that could reach the port read the install and home paths. The miss keeps #445's hint, without the roots. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…es a warning's kind joined_audio_short_after_mux fired on any saved video with shots and audio, and always said the track had been padded before the mux trimmed it. Past AUDIO_FIT_TOLERANCE_SECONDS the save-time fit leaves the track alone, so that explanation was false, on top of the audio_video_length_mismatch warning that already names the gap. get_job_events(kinds=...) matched only an event's `event`, so a warning type (phase_stall, audio_clipped - event "warning", kind "...") selected nothing though the tool's own docstring names those kinds. It now matches `event` or `kind` (dw.events.select_kinds). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#428 and #423 documented a post-mux residual with no warning and a last shot a sample or two off the grid; #435 and #426, merged the same night, warn on that residual (joined_audio_short_after_mux) and remeasure the shots against the written file. The tasks and workflow guides and pair_audio's docstring now say so, and name joined_audio_padded_to_frames and match_levels_near_silent. loop_frames' docstring says its float32 frames are for reference conditions, not keyframes (#444). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
assemble-and-score and dissolve-between-shots warned on every ordinary run: a 1-sample joined_audio_padded_to_frames, a 15-sample joined_audio_short_after_mux, and (with inputs that all agree and a pinned sample_rate) a sample_rate_mismatch claiming the inputs disagree. - a pad or post-mux residual under one frame is rounding: padded or re-measured as before, logged (emit_log), not warned - the line pair_audio's unfitted check already draws (LENGTH_WARN_MS); - concat_videos and dissolve_videos warn about sample rates only when the inputs disagree; converting agreeing inputs to a pinned rate is logged. The resample itself moved out of the warning branch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…o component key out of the snapshot CodeQL's dw/path-injection flagged 14 flows on the 0.4.0 PR. Each was already contained by a check the query does not model (validate_path on the returned file, Hub's repo-id shape), so they now go through the modeled validators as well: - recorded_shots and the describe route validate the run and identity directories against the workspace outputs; - repo_download_incomplete validates the repo's cache folder; - validate_media_path joins the query's list of path validators. One adjacent flow was real and unflagged: a key in a cached repo's model_index.json was joined onto the snapshot unchecked, so an absolute or '..' key turned validate_workflow's downloads_required into a directory-existence oracle. Such keys are now skipped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…esolvers resolve_path_references hands a non-reference string back unchanged, so CodeQL followed the probe's value through it to os.path.isfile. The probe only calls it for asset:/output: values: call fetch_asset/fetch_output directly, as it does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Release 0.4.0:
develop@e86fc20intomaster(262 commits since 0.4.0-beta.7).Gates (all green on
e86fc20)scripts/preflight.shon3a28146: ruff clean; 6,585 pytest, 335 UI unit, 108 Playwright e2e passed. Full pytest one86fc20: 6,592 passed. CI and CodeQL one86fc20pass, with no new alerts relative tomaster.3a28146(security 37 cases, smoke 68, complete 109): no security failures. Issues get_task: argument renamed name -> command (suite/docs still use the old name) #447–C-F108: dissolve of a multi-shot previous_result: expands inner shot names instead of collapsing to the step name #460 were filed; none blocks the release. They are suite drift, two performance readings and one metric bug, plus C-F020: dissolve-between-shots warns on identical-rate shots due to template's pinned sample_rate (likely stale suite expectation post-#287) #453/C-F029: templates/assemble-and-score warns on routine internal trim/pad, expected warnings: [] #454, which are fixed here. They go to the agent loop after the release.0d3a6c9(nothing new) and smoke one86fc20(nothing new).assemble-and-scoreanddissolve-between-shotson the C-F029: templates/assemble-and-score warns on routine internal trim/pad, expected warnings: [] #454/C-F020: dissolve-between-shots warns on identical-rate shots due to template's pinned sample_rate (likely stale suite expectation post-#287) #453 inputs now finish withwarnings: []./outputsasset misses no longer name server paths;kindsfilter;model_index.jsoncomponent keys held to the snapshot.After merging
scripts/release.sh 0.4.0onmaster.gh release edit v0.4.0 --notes-file <the section below>.masterback intodevelopand bumpdevelopto the next version.Release notes (from docs/RELEASING.md)
Breaking and behaviour changes
download_outputover adw.serve --mcpendpoint refuses a call with nodestination. It used to write into the server's own directory (export_job/list_gallery return origin-less URLs and export_job tells an MCP-only agent to fetch them; mounted download_output strands files in the workspace root #353).Untrusted workflows are refused in more cases (#407 stage 1: trust gate and URL host #409-#407 stage 5: pixel limit on decode #413):
*_typethat doesn't resolve to a class, or that isn't a kind aworkflow constructs: a diffusers or transformers model, pipeline,
scheduler, tokenizer or processor, a quantization config, an auto
factory, a diffusers reference/condition type or an attention processor.
A plain
torchclass such astorch.nn.Linearis now refused;constant:walks through_names or out of the allowed packages;text/htmlandtext/xmlresult types;and so Tailscale);
Listings and export zips drop symlinks that escape their root.
--trust-workflowslifts all of these.run_workflowvalidates the caller'sargumentswhen it queues the job(run_workflow queues a job whose argument-resolved result.content_type is text/html; validate_workflow refuses it #414/run_workflow refuses a document-default text/html content_type that the caller's argument overrides; validate_workflow accepts it #415).
validate_workflow(arguments={})checks a run with no valuessupplied, not just the document (Required task argument fed by a null-defaulted variable is reported as "the step does not supply" it, and save_workflow refuses the document #364).
A fractional value for an int variable is refused (Variable coercion silently truncates a fractional argument to an integer-defaulted variable (score_gain 0.3 -> 0 deleted the score) #338), and so is a
still image passed as a video argument (loop_frames invites a still image for
video, but a .png path/asset there fails at run time ("Video file extension not allowed: .png"), and validate passes it #347).templates/minimax/musicnormalizes to -3 dBFS instead of -1, so its outputis quieter (templates/minimax/music: balanced step at peak_dbfs -1.0 still clips after mp3 encode (+0.56 dBFS); engine's own warning prescribes -3 #362).
Every response carries
X-Content-Type-Options: nosniffandX-Frame-Options: DENY. Active document types under/outputsand/inputsare served withContent-Security-Policy: sandbox.A validate-time probe reads only a literal media path that the run itself
would be allowed to read.
A dict or list passed to a string-typed variable is refused (validate_workflow passes a dict for a string location variable (templates/ltx2/keyframes first_image); run fails with repr-as-path #433).
templates/ltx2/keyframestakesfirst_image/last_imageas plainstrings, not
{"location": ...}(templates/ltx2/keyframes fails with its own defaults: Unsupportedframestype for condition 0: dict (validate clean) #431/validate_workflow passes a dict for a string location variable (templates/ltx2/keyframes first_image); run fails with repr-as-path #433).loop_framesreturns float32 frames in [0, 1] instead of uint8, the shapeLTX2ReferenceConditionneeds; a keyframe condition still wantsframes_as_array.ltx2/reference-sheet's default asset is nowasset:reference_sheet.jpg(ltx2/reference-sheet: succeeds with no warnings but every frame is blocky mush; default asset:reference_sheet.png missing #444).validate_workflowrefuses acomponentsname the pipeline doesn'tregister;
duration_headis gone from the in-context LTX-2 templates(templates/ltx2/restore-decompression fails at load: LTX2InContextPipeline has no component 'duration_head' (validate passes) #442).
A
{"media_type": "image"}reference on a video argument loads as aone-frame still (frame_grid: validator suggests {media_type: image} for stills; validates clean, fails at run time 'Video file extension not allowed: .jpg' #443).
pair_audio fit: "video"always fits, and warns on any nonzero gap(pair_audio fit="video" (and slice_audio past the end) leave a track 15 samples short with no pad and no warning #428/pair_audio fit=video warns 'last part of the cut has no soundtrack' for a 1-sample (0.00 s) pad from ordinary rate rounding #429).
concat_videosanddissolve_videospad a short joinedtrack to the frame grid, warning (
joined_audio_padded_to_frames) onlywhen the pad is a frame or more; a residual the AAC mux trims off is
logged, or warned as
joined_audio_short_after_muxfrom a frame up.media.shotsis measured against the file as written (concat_videos: after a resampling join the shot map overruns the written file by 30 samples (assess_output shot_span_overrun on a stock join) #426/dissolve_videos after an ltx2/keyframes clip: shot map ends 16 then 32 samples short of the frame grid, no pad/warn (#428 class) #435/C-F029: templates/assemble-and-score warns on routine internal trim/pad, expected warnings: [] #454).Neither warns about resampling inputs that agree to a pinned
sample_rate(C-F020: dissolve-between-shots warns on identical-rate shots due to template's pinned sample_rate (likely stale suite expectation post-#287) #453).New warnings:
match_levels_near_silent(match_levels gains a near-silent input +29.9 dB with only a log line, no warning (dissolve_videos, ep65) #434), andshot_span_overrunfrom the probes plus a validate-time check (analyze_seams: a shots= record running past the file's end (124+300 on a 248 f video) is clipped silently, no warning #425).
Error text changed:
delete_workspace(delete_workspace refusal names a nonexistent 'acknowledged=true' parameter before the correct acknowledged_cost #437/delete_workspace on a nonexistent workspace tells the caller to repeat with acknowledged_cost=True #438), the sub-workflow pathrefusal names the places it looked (SE-F023: sub-workflow path refusal message doesn't list where it looked #422), and
/outputs/asset:...missesname the asset without server paths.
New
assess_outputtool andGET /api/gallery/{name}/assess, plus theprobe tasks
analyze_shots,analyze_seamsandanalyze_sync_drift(#378 stage B: probes as tasks (analyze_shots, analyze_seams, analyze_sync_drift) and the rules table #387/#378 stage C: assess_output MCP tool, route, and the guide section #388).
media.shots).get_output_frames(seams=true)uses them, so it no longer needsboundaries(#378 stage A: record shot boundaries on joined video (manifest, metadata, seams without boundaries) #385).v<N>):list_galleryreturnsrun_id/versionand filters byfolderandversion;output:<wf>/v<N>/<file>references;wait_for_jobreturnsrun_version;<wf>-vN-<job>.zip.list_gallery(media=true)adds durations, andoutput:names work ingallery reads (Gallery reads reject an
output:-prefixed name with a misleading "path does not exist"; list_gallery can't show a take's duration #356).DW_PUBLIC_URLadds absolute URLs to gallery and export responses.export_jobalso returnsauth_requiredandopen_url(export_job/list_gallery return origin-less URLs and export_job tells an MCP-only agent to fetch them; mounted download_output strands files in the workspace root #353).gradetask for images and video: exposure, contrast, saturation andtemperature/tint (Add a CPU grade task (v1: exposure, contrast, saturation, temperature/tint) for images and video #349).
templates/minimax/shots-batchH3 template (Add templates/minimax/shots-batch: list-driven H3 shot generation with no in-job assembly #352).seedargument (Catalog seed convention: every generative entry takes "seed": "variable:seed" (pinned ones keep their value; unseeded ones default null) #351).normalize_audio(target_lufs), andintegrated_lufsplus true peak inmedia metadata (Loudness: report integrated LUFS + true peak in media metadata, and add target_lufs to normalize_audio #361).
gain_audiowith no region gains the whole track (gain_audio with no region validates clean, then fails at run time; the schema marks every region arg optional #395).world_fade_out_msonassemble-and-score(assemble-and-score / dissolve-between-shots: expose a world_fade_out_ms so a score's written ending isn't buried under the world track #339).phase_detail(A download a job triggers is invisible: 17.8 min ofphase_stallwarnings, nothing in list_downloads, no byte progress #343).phase_stalleventsnow read as informational (phase_stall events read as a fault during phases the H3 skill calls healthy, and they bump event_count so they look like liveness #357).
workflow,inline_workflowandpromptalso accept a JSON string. Amistyped workflow name gets suggestions from the catalog (get_workflow: 'Unknown workflow: dialogue-short' gives no hint that the catalog name is templates/minimax/dialogue-short #397).
at
clear_memory.get_job_events(kinds=...)and?kinds=on the event-log route; a kindmatches an event's
eventor itskind, so["phase_stall"]selectsone warning type (get_job_events has no kind filter: confirming 4 applied-value log lines costs ~20 KB, most of it memory events #436).
get_memoryreports the step cache'sentriesandretained_bytes(Step cache: a stale-entry eviction never subtracts its size, so _retained_bytes only grows #418).
get_output_imageand/outputsresolveasset:references (get_output_image: asset: reference returns bare 'Not Found' (siblings accept asset:) #445), andget_output_frames(seams=true)works on linked assets (get_output_frames(seams=true) refuses a linked asset whose media.shots get_gallery_metadata/assess_output both read #430).assess_outputlists each finding once (assess_output compact: the same shot_span_overrun finding is listed 3 times #427). Shots are named bytheir source when joined inputs already carry shots (concat_videos: previous_result input named "video 2" again when an earlier input carries inner shots (#396 regression on multi-shot inputs) #432).
basis: observed(validate_workflow: dissolve-between-shots at 3 shots quotes basis:catalog though get_workflow shows 24 observed runs at drivers {shots: 3}, no reason given #439). The Music 3 hint no longer shows on video(get_gallery_metadata: next hint on a video output includes Music 3 ceiling guidance #441).
Fixes
templates/ltx2/keyframes(templates/ltx2/keyframes fails with its own defaults: Unsupportedframestype for condition 0: dict (validate clean) #431),restore-decompression(templates/ltx2/restore-decompression fails at load: LTX2InContextPipeline has no component 'duration_head' (validate passes) #442) andreference-sheet(ltx2/reference-sheet: succeeds with no warnings but every frame is blocky mush; default asset:reference_sheet.png missing #444) run with their own defaults again.(dissolve-between-shots: last shot's num_samples is 1 over its frame span (227851 vs 227850) at 44.1k/24fps #423, concat_videos: after a resampling join the shot map overruns the written file by 30 samples (assess_output shot_span_overrun on a stock join) #426, pair_audio fit="video" (and slice_audio past the end) leave a track 15 samples short with no pad and no warning #428, dissolve_videos after an ltx2/keyframes clip: shot map ends 16 then 32 samples short of the frame grid, no pad/warn (#428 class) #435).
🤖 Generated with Claude Code