Repository navigation
Conversation
…ws the cut #141: a task step that left a *required* argument unset validated as `valid: true` and then failed the job on Python's own "resample_audio() missing 1 required positional argument: 'audio'" - the one class of mistake a free pre-flight most obviously exists for. An unknown argument sat beside it as a warning, so a step with every argument it was given rejected and every argument it needs missing still validated. `task_signature_errors` (dw/introspection.py) reports both, at the JSON path each sits at, from the same introspection `get_task` already answers `required: true` from; the command itself refuses the same thing at run time, in the validator's wording rather than Python's, for a value that arrived from a variable or an earlier step. The unknown-argument warning is dropped - reported once, by the pass whose verdict it changes. No catalog workflow trips either check. #142: music-video sliced a hardcoded 496-frame soundtrack (4 shots x 124) while everything else in it followed the `shots` list, so a two-shot run laid 20.7 s of song over 10.3 s of picture and reported `succeeded` with `warnings: []`. `pair_audio` takes `fit: "video"` now: the track is cut to the frames it is laid over, or padded with silence and warned about when it is shorter. The template hands it the whole song and the slice step is gone, so the soundtrack follows the list whatever length it is; without `fit`, a length that disagrees with the frames' is warned about rather than passing in silence. Implementer agent, model opus via provider anthropic.
Approved proposal docs/proposals/unreferenced-step-elision.md. Before the first step executes, dw/elision.py drops any step whose result no later step reads and which writes no file. `dialogue-short` cast from portraits that already exist ran its two Z-Image steps anyway and threw the pictures away - about a minute of GPU and two model loads per episode (#109). Four guardrails, as approved: a step that saves is kept (a `result` with a content type and `save` not false - exactly what Result.save asks); the last step is kept; a release moves onto the last surviving step before it (`release_pipeline` only when that step loaded the same pipeline, since moving it elsewhere would unload something the workflow never asked to unload; `release_models` always); and every elision is an emit_warning, so a misspelled reference shows up as "draw_character_a did not run" rather than as a silently different picture. Elision is transitive and runs to a fixed point. `build_plan` elides too, so `steps`, `downloads_required`, the estimate and the fingerprint are the work that will actually happen, with the dropped steps listed under `elided_steps`; the run manifest records the same list; the editor's plan lines name them. With the engine change, per Don's condition: `dialogue-short`'s two draw steps carry `save: false`, so a `from_file` cast actually skips them - and `draw_character_b`'s `release_pipeline` is dropped rather than moved when `draw_character_a` goes too, because nothing was loaded to release. The default (uncast) run is unchanged, and a test sweeps every catalog workflow to keep it that way. Test fixtures that used an unreferenced non-saving step to test something else (the plan, the step cache, pipeline release) now declare a result or a reference - noted at each. Implementer agent, model opus via provider anthropic.
…oads `validate_workflow(num_frames=61)` on an H3 template answered `valid: true`, then the run loaded the weights and the turbo LoRA and failed 138.7 s later on a check against two integers. Every term in that check is a property of the model, so the rule is now declared on the workflow and checked for free. `variable_constraints` (dw/variable_constraints.py) takes a chain step's `frame_snap` field names - one shape, not two - and a chain writes `"frame_snap": "constraint:num_frames"` rather than repeating the numbers. Checked in `validation_errors` (so POST /api/validate, validate_workflow and the pre-queue check all refuse at `arguments.<name>` / `variables.<name>`), at run time in `apply_constraints` before anything loads, and reported beside the default by `list_workflows` (terse) / `get_workflow(variables_only=true)` - the half that stops the next consumer picking 61. The bounds hold for the value the run will *use*, matching diffusers' `align_num_frames`, which snaps before it range-checks: 108 is accepted (it becomes 124), 346 refused (it would become 362). H3's templates declare `snap: "up"` and warn through `emit_warning` when a count is rounded - the silent half of #96. LTX-2.5's declare the `8 * n + 1` grid with *no* snap, because those pipelines floor an off-grid count: rounding up would be a second silent change to the length. tests/test_variable_constraints.py sweeps the whole catalog and pins every declared number to the diffusers symbol it derives from, rather than asserting per template. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`list_workflows(shape="shot", traits="identity-referenced")` answered `cost: null` for seven of eight entries on a box that had run `templates/minimax/reference-to-video` ten times at one size. `cost` is defined in the schema as never derived, so this is a sibling rather than a second meaning for it: `observed` (dw/server/observed_cost.py), built from this server's own jobs.sqlite rows and never a substitute for a maintainer's claim on a named card. Four rules, each a way the naive median would lie, each with a test: - Runs bucket by the workflow's declared `cost_drivers`, so a 345-frame run never informs a 124-frame figure; the bucket reported is the one the defaults give, keeping it comparable to a curated figure. A *list* driver buckets on its length - two four-shot runs are comparable however different their prompts. No drivers falls back to default-arguments-only. - `cold_minutes` and `warm_minutes` are separate, each with its own run count. Only the cold one is comparable to `cost`, which is wall clock including model load. There is no unqualified figure. - A run whose every manifest entry is `reused` wrote nothing and is out. - A run whose persisted events hit the 200 cap without a `loading` phase is `unclassified_runs`, not assumed warm. Everything comes off the job row in one query (`JobHistory.finished_runs`), so a figure survives a pruned run directory, and the aggregate caches against `watermark()` rather than a file mtime - a job landing changes every figure and changes no file. Compact listing carries `observed_minutes`/ `observed_runs` only, at the #101 budget; the full block is in the full listing and `GET /api/workflows/{name}/variables`. `cost_drivers` is declared on every workflow carrying a curated `cost` and on every MiniMax and LTX-2.5 template; `tests/test_observed_cost.py` sweeps the catalog for one naming no variable, which would bucket on nothing while looking like it worked. Against lem's real history the figures check out: reference-to-video 8.04 min cold over 10 runs against a curated 8.0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…undtrack, declared bounds, observed cost
Caught deploying to lem: the device lookup imported `get_memory_stats`, which is spelled `device_memory_stats`, and one try block around both calls turned that ImportError into `device: null` / `name: null` on a box plainly running on CUDA. That is the silent-null shape this field exists to replace, so the two lookups are now separate - a card name only CUDA reports, absent without taking the device type down with it - and a test pins both symbols. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The release path added in #98's neighbourhood ('step was redefined - releasing its previous pipeline') reads self._prior_step_keys, and nothing ever passed prior_step_keys to Workflow.run. A command builds a fresh Workflow every time, so the map was empty on every job and the release could only ever fire inside one run - which is why a rerun of the SAME workflow with different arguments (a LoRA scale, a canvas, a step count all change what the step loads) re-loaded on top of the resident stack and was OOM-killed with no 'Released cached models' line to show for it. The worker now carries the step -> cache key map between commands, merged rather than replaced so a run that died partway does not forget what its later steps loaded, and cleared on a workflow switch where _cleanup_all has already dropped what those keys addressed. The release emits pipeline_released with reason 'superseded' - without it a reload on top of a resident model is indistinguishable from a cold load on the event stream, which is what made this take three jobs to tell apart from a CUDA OOM.
…exists The <Picture 1> every shot conditions on sat in steps[].pipeline.arguments.references, so no 'arguments' value could reach it: reusing a standing cast member meant fetching the definition and resubmitting the whole ~120-line template as an inline copy, which stops tracking the template and prices as 'unknown' rather than from its measured history. It is now one variable, 'singer_reference', holding the reference object itself and naming draw_singer by default - the same shape dialogue-short takes per shot entry, and existing machinery throughout: a dict-valued variable splices into the list and resolve_variable_values resolves the reference type inside it. A caller passing a 'from_file' reference leaves draw_singer unreferenced, and elision drops it before the run. draw_singer's result carries 'save': false, as dialogue-short's two draw steps do. Without it the deliverable guardrail keeps the step and a cast episode pays for a portrait nothing looks at - which was the whole point. It does mean a default run no longer writes the portrait into intermediate/; the seed makes it reproducible, and a portrait worth keeping is kept from the run that drew it.
…workflow's to state A MiniMax-H3 turbo checkpoint comes with a canvas, a sigma shift and a network alpha, and they move together. dw pinned one checkpoint and exposed none of the three, so the 768p LoRAs that have existed since 2026-08-27 were unreachable: swapping lora_weight_name alone would run them on the 544p sigma schedule (the base scheduler ships shift 12.0) at a sixteenth of their trained strength. #147 - the engine already took 'scheduler'/'audio_scheduler' shifts; every H3 template now declares both, as video_shift/audio_shift. Alpha is new: a lora entry takes 'alpha', applied to the peft layers before set_adapters, which is what recomputes each layer's scaling from it. That reproduces upstream's effective_scale = lora_scale * alpha / rank exactly. An alpha that matches no loaded layer is refused rather than silently scaling nothing. Why alpha had to be a knob rather than a default: all three published checkpoints record alpha 8 at rank 128 in their __metadata__ and diffusers honors it, which matches upstream's own default - but upstream's 768p FL2VA invocation passes --lora-alpha 128, sixteen times that. #148 - templates/minimax/video-with-audio-768p: 1344x768, shift 6/3, alpha 128, on the 8-step 768p FL2VA LoRA. 544p stays the fast-iteration path. No cost block: nothing has measured it. #149 - every ref2va template now loads minimax_h3_ref2v_turbo_8step_v1.0_768p_bf16. Six carried no LoRA and ran 20 steps because the only turbo LoRA was distilled against the base transformer; four (storyboard, dialogue-short, music-video, chain-matched-and-aligned) carried the FL2VA one, which upstream says outright not to do on the ref path - a ref2va step holds transformer_ref alone, so diffusers routes it there and nothing complains. A test refuses an FL2VA weight on a ref2va step. Nine steps, not eight: the scheduler counts sigma grid points with the terminal zero among them, so an 8-step checkpoint wants num_inference_steps 9. Pinned against set_timesteps. cost: composable-references (27.4) and reference-to-video (8.0) were measured at 20 steps and went with the schedule; the three whose step count and canvas did not change kept theirs. tests/test_h3_schedule.py sweeps the family and pins every number to the diffusers symbol it derives from.
…n be measured LTX-2.5 ships a second video decoder - LTX2VideoDiffusionDecodePipeline, a small diffusion model that denoises pixels from a context volume rather than deconvolving latents - and upstream treats it as the production path. Nothing in the family referenced it, and whether it is worth its cost here is an open question that needs a run rather than an argument. This is that run made possible, and nothing more. The generation step is text-to-video's, argument for argument, stopped at output_type 'latent'; the decode step is the only difference, so at the same seed and sigmas the two templates are comparable frame for frame and minute for minute. No cost block - nothing has measured it. Two honest limits, both in the description: it is silent, because at output_type 'latent' the soundtrack comes back as audio latents and this decoder is video-only; and the decoder is a second model on top of the 22B transformer, so the generation step releases its pipeline first. The template has never been run - the tester's first run is the experiment. What is pinned is that it drives the pipeline correctly: every argument against the real __call__ signature, 'denormalize': false against the denormalize LTX2Pipeline itself applies on the latent path, and previous_result:latents.frames against the output field that carries them.
…the superseded-pipeline release, the music video's singer, LTX-2.5's other decoder
…adapter is checked against its partition, an elided step says who decided it, and a clipped deliverable warns #154: plan.estimate consults dw/server/observed_cost.py before the curated cost block (basis "observed", with runs). Cold median only, this backend only, and only for the bucket the caller's own arguments fall in - ObservedCosts.observed takes arguments now, so a resized list falls back to the curated figure rather than quoting the default list's minutes. No child cost is added: an observed run already ran the children. #155: dw/adapter_compatibility.py refuses an FL2VA-trained LoRA on a ref2va step (and the symmetric mistake) in validation_errors, at arguments.<name> when the caller supplied it. A weight_name naming neither path is a warning, since a future reference-trained checkpoint cannot be predicted. Workflow names and partitions pinned to diffusers by tests/test_h3_adapters.py. #157: a step elided because a caller replaced the variable that read it carries overridden_by and drops the misspelling diagnosis (overriding_variables in dw/elision.py) - #146's happy path warned that draw_singer probably had a typo every run. #158: warn_without_headroom in dw/result.py says when a saved track or a muxed video's soundtrack peaks at or above -0.5 dBFS, and get_gallery_metadata's hint now teaches both ends of the range. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…H3 adapter check, the elision wording, the audio headroom warning
… run, and the Music 3 templates leave headroom #153: measured on lem why templates/ltx2/diffusion-decode OOMs. Both allocations come from diffusers' portable FlexAttention fallback for the decoder's 3D neighborhood window - create_block_mask densifying seq_len**2 and then an uncompiled flex_attention falling to the eager reference path - and neither is reduced by tiling (stages 1-3 always see the full volume) or by shrinking the clip (the stage-4 grid tracks output resolution). At 224x224x25, the smallest canvas its 7x7 kernel accepts at all, it still asks for 8.08 GiB. The path that works builds no mask: NATTEN's na3d. A component configuration can name its attention processor now (attn_processor_type, beside enable_tiling), the template names the NATTEN processor, and kernels is a dependency so the failure names the missing build rather than a missing package. No shi-labs/natten build exists for torch 2.14, which is the experiment's answer on this box and is written into the template, the 24GB recipes and the skill. #159: the headroom warning #158 added was firing on the default path of both Music 3 templates, every run - a caller who did nothing wrong got a clipped deliverable and a note telling them to add a step. They carry the step now, at the -1 dBFS assemble-and-score has always used. music-video normalizes only the track going into the mux, not the slices that condition the shots, so the picture is byte for byte what it was. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…th, and headroom on the Music 3 deliverables
…easured The first numbers were taken with the server's worker holding ~7 GiB, which made the smallest canvas look marginal. On an otherwise empty 3090 it needs about 25.5 GiB, and 512x288x25 reaches the flex score matrix rather than stopping at the mask: 69.77 GiB. Nothing fits. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…mber's own file can be named back #145: a `variable_constraints` key stays a plain variable name and is now matched wherever a value by that name sits - a top-level variable, or a field of a `for_each` entry where some step consumes it as `item:<name>` (`entry_constraint_fields`). All three layers answer at the entry path: refusal at `arguments.shots[0].num_frames`, the snap warning at the same place, and the run-time pass rounding the entry in place. `dialogue-short` declares the H3 block it could not hang anywhere before, and the rule is reported beside the field in the catalog's `lists` as well as in `constraints`. Limit 2 of preflight-argument-bounds.md records its supersession. #162: '@' is now legal inside an output and asset name - a `for_each` member is '<group>@<entry>' and its files carry that, so files the server named could not be named back to it. A malformed reference is refused in the free pre-flight (`dw/reference_names.py`, wired into `validation_errors`) rather than after the queue, and the refusal names the character and position it objected to rather than only describing a valid name. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ets the headroom the encode takes #159 normalized music-video's soundtrack to -1 dBFS and the finished mp4 still decoded at +0.94: the AAC encode overshoots by around 1.9 dB on this material where an mp3 overshoots by a tenth of one, and the check that should have caught it was reading the waveform handed to the writer rather than the file that came out. Both halves. warn_if_written_above_full_scale (kind 'audio_clipped') probes the file it just wrote and warns when it decodes at or above 0 dBFS, for audio and muxed video only, and stays silent when warn_without_headroom has already spoken for that file. And every template whose deliverable ends in a pair_audio mux - music-video, assemble-and-score, dissolve-between-shots - normalizes to -3 dBFS; music, an mp3 deliverable measured below full scale, keeps -1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…estoration routes The gates on the three IC-LoRA repos have been accepted, so the cards are readable and the recipes are the vendors' rather than guesses from the upscaler's shape. #151 reference-sheet: Ingredients, the family's only identity route - a reference sheet held across the clip. The sheet is a still and the LoRA reads it through a 121-frame bucket, so a new loop_frames task (the video analogue of loop_audio) laps it into a static video; reference_frames carries a declared floor of 121. #152 restore-deblur and restore-decompression: the first templates here whose reference is a clip dw did not generate. Each inverts one defect and says so. All three at reference_downscale_factor 1, at their trained buckets, with their trained caption forms stored under prompts/ltx2/ and tagged ic-lora - a conditioning caption is a different genre from a shot caption, so the prompt-library test checks each against its own convention. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The backend was already complete: GET /api/assets spans the workspace's own library, the shared common one and any examples library, each entry tagged with its origin. What was missing was any way to see it - discovering what the library held meant reading an asset: failure or grepping the box, and nothing showed which names a shared or example library was shadowing. AssetsPage.svelte is the gallery's UX on the input side: folder groups, a contact-sheet grid, a detail popout, upload, delete and the reference to copy. The origin badge sits on the tile rather than only in the detail, because 'why can I not delete this' should be answerable at a glance - an examples asset is read-only and gets no delete button at all rather than a 403. api.ts gains listAssets/deleteAsset and uploadMedia grows the asset_name and shared arguments the endpoint already took. Also fixes a stale Plan fixture in plan.test.ts that had npm run check red on develop. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…s, the LTX-2.5 IC-LoRA templates, written-level headroom, '@' in output names, and the assets page
… the field The compact listing carried it in its 'lists' block; the variables view, which is where a caller reads the defaults before setting one, carried only the top-level 'constraints' key - which for dialogue-short names no declared variable and so reads as a rule about nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Found verifying #151: templates/ltx2/reference-sheet on lem, which holds every LTX-2.5 base weight but has never pulled the Ingredients IC-LoRA, answered downloads_required: [] - and would then have pulled it mid-run. An adapter names its repo under model_name directly rather than inside a from_pretrained_arguments block, so _collect_sources walked past it. The same gap covered generative-upscale's spatial upscaler. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The shared `common` library and every `--examples-dir` one sit on every workspace's search path, so most of the grid can be identical in two workspaces - on a real box 34 shared assets against a handful the workspace owns. Switching the picker then looks like a page that ignored it. Three things say so rather than leaving it to be inferred: - the count breaks out `N from other libraries` - the hint names the rule - the library pick is offered whenever *anything* came from elsewhere, not only when two origins are in play. It used to unmount exactly in the workspace where the question comes up - every asset `common`, one origin, no control - and filtering to `workspace` is what shows you what is actually yours The upload destination becomes a named pick (`upload to [this workspace | shared library]`) rather than a bare `shared` tickbox. It is the one thing about an upload that cannot be changed afterwards and nothing on the page explained what the tickbox meant. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…hree archives The gallery's bulk selection had been copied into the assets page whole, and the copies had already drifted: the assets page's Escape guard knew that ConfirmDialog renders `alertdialog` and the gallery's did not, so Escape in the gallery's delete confirm cancelled the dialog *and* closed the detail behind it. That is what two copies of a keyboard contract does. It is now `picks.svelte.ts` - a `Picks` over a getter for the grid's current order, so a filter changing under the selection is seen rather than snapshotted, plus `actOnEach` for the sequential act-and-collect-failures loop and `dialogOpen()` for the guard - with `BulkBar.svelte` for the sticky bar and select-all, and the two rules that have to reach a tile the page lays out in app.css. The two pages lose 314 lines between them. The gallery's Escape bug is fixed by construction and pinned by a test. Server: the temp-zip dance - and specifically the `except BaseException: unlink` that stops a half-written archive leaking into tmp - was in three routes; it is now `_zip_download`. `AssetArchiveRequest` was a byte-identical copy of `ArchiveRequest`. `archive_assets` rebuilt the asset search path once per name, so a thousand-name archive stat'd every root a thousand times; `_asset_in` takes the path already built, and `_asset_file` delegates to it so the two resolvers cannot drift. Archives now store rather than deflate anything but `.bmp`/`.wav`. Every other extension the libraries hold is an already-compressed container, so deflating buys ~0.03% for a full CPU pass - and since the response does not start until the temp file is complete, that pass is latency the caller waits through: measured, about 13s for a gigabyte of video. Also: `originOffered` is `borrowed > 0` rather than that OR'd onto the rule it replaced; the upload destination is read straight off `uploadTo`; the header count's separator is one expression, since Svelte trims a block's leading whitespace and had eaten that space twice; and AssetsPage's tests reset their mocks rather than clearing them, so an implementation set by one test cannot leak into the next. Left alone deliberately, as open questions rather than cleanups: `size` counts every ticked name while the actions run over the visible ones (`Picks.hidden` is there for whichever way that is settled), the bulk-delete confirm does not say that a shared asset goes for every workspace, and the upload destination offers a shared library on servers that have none. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
_asset_in resolved each root by calling resolve_asset_reference, which
already walks the pinned fallbacks on its own - 107 syscalls per hit
where 29 would do. Validate the name once and let each root be a plain
join-and-isfile check instead, via a new _resolution_roots(ws) helper
shared with the validate route and _asset_file.
The compression policy inverted itself for anything outside {.bmp,
.wav}: COMPRESSIBLE_EXTENSIONS named the two formats that deflate, but
the check stored everything *not* in that set - including plain text
like workflow.json, manifest.json and the export README. Replace it
with RAW_MEDIA_EXTENSIONS (what it actually named) and store only a
recognized media extension that isn't raw.
archive_assets now strips and dedupes names before resolving, so a
repeated or whitespace-varied selection collapses onto one zip entry
instead of colliding on write. The two archive routes' duplicated
three-line tail (timestamp, log line, _zip_download call) is now
_archive_selection(entries, kind), logged after the archive is
written so a failed write never logs a false success.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Picks.size counted every ticked name, including ones a filter was hiding, so the bar's number and what Delete/Download would actually act on (names, which is visible-only) could disagree. keepFailed made it worse: it cleared the whole selection and re-added only the failures, silently dropping a hidden tick the action never attempted. size now returns names.length, hidden is gone (nothing rendered it), and keepFailed(attempted, failed) only removes names it was told were attempted and did not fail, so a hidden tick survives. Both pages early-return when there is nothing visible to act on, for the keyboard path that could still reach the handler with the bar hidden. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A client that only sees the resolved asset list cannot show which of its names hide a shared one, or say why a delete lands on one library and 403s on another. `libraries` reports the search path in order with origin and writability; `shadowed` reports the entries the resolution loop used to silently skip - same shape as an `assets` entry minus `url` (that URL would serve the shadowing file), plus `shadowed_by`. MCP `list_assets` returns this body verbatim, so its docstring and docs/MCP.md/docs/SERVER.md describe the new fields too. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
validate_workflow used workflow/name while run_workflow used inline_workflow/workflow_path for the same two concepts, forcing an agent that just validated a document to rename its keys before running it. Both tools now accept both spellings: validate_workflow gains inline_workflow/workflow_path, run_workflow gains workflow/name. Passing both spellings of the same concept is a clear error, and exactly-one-of enforcement is preserved across the resolved (post-alias) values. Tool docstrings cross-reference the other tool's naming. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ing it, and warn on a sample_rate/file-rate mismatch assemble-and-score's soundtrack step fed the mix rate into slice_audio as an override, which reinterprets a file's samples at that rate rather than resampling them - silently changing the score's speed and pitch whenever its native rate differed from 44100. The template now slices the score at its own rate and explicitly resamples it afterward, mirroring the existing 'world' step. Separately, _waveform_and_rate now warns (kind: rate_override_mismatch) whenever a given sample_rate disagrees with the rate a named file or video actually carries, so the same mistake elsewhere in the catalog is no longer silent. Model: sonnet (Anthropic)
Assets page
The watchdog emits its report as a warning event, and Job._note_progress persisted every warning message; each tick carries a different seconds value so none deduped, and a normal 60-140 s cold load left 2-4 stall lines on a succeeded job. The event log keeps them; warnings does not. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… index _asset_roots drops a workspace library that does not exist yet, which made an examples tree root 0 - and root 0 was labelled workspace, writable, deletable. The Assets page's select-all + Delete made that reachable. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
_resolution_roots fell back to [ws.assets] when the search path was empty, which is [None] on a server configured without one; _asset_in then joined None. The 'no asset library' 404 branch was unreachable. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…s a result Its child writes its own files, so the parent's result block says nothing about whether the step produces output; treating it as saving nothing let an unreferenced composing step vanish and the job succeed without it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Extension-based detection listed every text-shape run as an orphan, and the MCP docstring tells the agent to list then delete. A run is orphaned when no file other than manifest/workflow/job.json survives under it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…n run is The /api/gallery route docstring and dw_mcp/catalog.py's internal list_gallery still described only_orphans by the old, wrong extension-based rule after the MCP tool docstring was corrected. A reader of either got the pre-fix definition. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…d as superseded Carrying step->key across jobs let a redefined step evict a pipeline its sibling still resolved to, reloading a resident model cold while both stacks were held. A shared key is left for the end-of-run sweep. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…nces resolve An entry field written as 'variable:x' is not a number until resolve_variable_values runs, so the run-time backstop skipped it and the value was substituted afterwards unchecked. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…e running workflow's steps _prior_step_keys is merged across every job the worker has run and never pruned, so a name it remembers can belong to an earlier, unrelated workflow's step. "Still shared" now checks only steps this run actually executes, so a stale cross-job entry can no longer save a superseded key from release. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…pe name Every validate, submit and rerun constructed the attention processor (a Hub kernel fetch) again; the answer is a per-process constant. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Construction is a Hub kernel fetch, which can fail transiently; caching that failure pinned every later validate/submit/rerun in the process to "broken" until restart. Only a clean answer is memoized now - a fault raises a private exception so lru_cache never stores it, and is re-probed on the next call. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
attach_observed asked ObservedCosts per entry and each ask ran the COUNT(*)/MAX(finished_at) query under the history lock - ~70-80 per list_workflows. refresh() once, then answer each name from the cache. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
_resolution_roots answers [] when the server has no asset library, and POST /api/validate handed that to over_roots, which re-raised its "first error" of None - a TypeError about BaseException reported as the validation message. The asset branch now answers the empty search path itself with the same "no asset library" wording the 404 route uses. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…step's current key is it The "still shared" guard asked whether another running step's PRIOR key was the key being superseded. When every step sharing one model variable changes at once - two steps, or a for_each group, on a rerun with a new model - each has the old key as its prior key and none will load it again, so the first step held the old stack while its replacement loaded: the two-stack transition #150 fixed. Workflow.run now records each pipeline step's current key from the realized steps (_running_pipeline_keys, the same dicts and function create_step_action hashes), and the guard holds a key only while another running step loads under it now. _running_step_names is gone; a stale name from another workflow is still not a running step. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
_iter_orphan_runs counted any name outside RUN_BOOKKEEPING_FILES as output, so a .DS_Store left by Finder made a run permanently non-orphan. A name starting with '.' is skipped in the has_output check. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
manifest.json and workflow.json were restated as literals beside the MANIFEST_FILE_NAME / REALIZED_FILE_NAME the module already imports. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…lision, orphans, #150 shared key, constraint order, kernel probe memo, one watermark per listing
Owner
Author
|
Merge-readiness pass (docs/superpowers/plans/2026-09-16-pr183-merge-readiness.md), landed on develop as c84199a + 603a0a7:
Full suite locally: 4945 passed, 16 skipped. CI: backend, ui, CodeQL all green. Follow-ups to file, not in this pass: 🤖 Generated with Claude Code |
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.
No description provided.