Skip to content

Release 0.4.0 - #461

Merged
dkackman merged 264 commits into
masterfrom
develop
Sep 25, 2026
Merged

dkackman merged 264 commits into
masterfrom
develop

Conversation

@dkackman

@dkackman dkackman commented Sep 25, 2026 •

Copy link
Copy Markdown
Owner

Release 0.4.0: develop @ e86fc20 into master (262 commits since 0.4.0-beta.7).

Gates (all green on e86fc20)

After merging

  1. scripts/release.sh 0.4.0 on master.
  2. gh release edit v0.4.0 --notes-file <the section below>.
  3. Merge master back into develop and bump develop to the next version.

Release notes (from docs/RELEASING.md)

Breaking and behaviour changes

New

Fixes

🤖 Generated with Claude Code

dkackman and others added 30 commits September 22, 2026 18:41
… 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>
#350, #366, #367 - processor-task introspection family: real
image-processor parameters, get_frame seeking instead of full-decode,
and distinct summaries for get_first_frame/get_last_frame.

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>
dkackman and others added 24 commits September 25, 2026 03:57
Fixes #436 (kinds filter on event-log/get_job_events) and #437
(contradictory acknowledged=true instruction in delete_workspace's
refusal).

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>
Comment thread dw/hub_cache.py Fixed
Comment thread dw/hub_cache.py Fixed
Comment thread dw/hub_cache.py Fixed
Comment thread dw/hub_cache.py Fixed
Comment thread dw/hub_cache.py Fixed
Comment thread dw/probe_paths.py Fixed
Comment thread dw/runs.py Fixed
Comment thread dw/runs.py Fixed
Comment thread dw/runs.py Fixed
Comment thread dw/runs.py Fixed
dkackman and others added 2 commits September 25, 2026 12:18
…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>
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.

3 participants