* fix(web): generation-agnostic Comfy draft-quota guard
"Failed to save workflow draft" toasts returned on ComfyUI frontend
1.44+ because draft storage moved to new localStorage key names
(per-workspace V1 keys, then a V2 index+payload scheme) that the old
exact-name guard embedded in ltx_director.js never matched - and the
1.44 V2 layer latches a session-wide storage-unavailable flag after one
failed save+evict cycle, re-toasting on every edit until reload.
Move the guard to its own global extension, web/koolook_draft_guard.js:
- match draft keys by prefix (Comfy.Workflow.Draft) across all known
generations, with unknown future keys as last-resort evictions
- rescue quota failures inside localStorage.setItem (evict the oldest
draft across generations, retry) before Comfy's own catch sees the
error, so the save succeeds and the latch never trips
- boot prune: drop the suffixed-V1 dead weight the frontend's V1->V2
migration never deletes, scope corrupt-key cleanup to the offending
key, drop oversized entries, enforce a total draft budget
- ship the guard with dev-sync-audio so a scoped sync cannot strand a
dev install guard-less
Behavior is covered by tests/js/test_draft_guard.mjs (12 scenarios,
wired into CI next to the existing node smoke test); static integration
contracts live in tests/sidebar/test_draft_guard.py.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(web): address deep-review findings on the draft guard
- latch the "still full" hint toast once per session so a genuinely
full origin cannot re-toast on repeated failing autosave attempts;
regression assertion added to the exhaustion scenario
- document the bounded-eviction termination contract and the
post-rethrow storage-unavailable latch at the wrapper, plus the
budget's V1-blob-only (1.5M) -> all-generations (2M) scope widening
(also now stated in the CHANGELOG entry)
- version-robust wording for the suffixed-V1 cleanup rationale: the
shipped 1.44 migration leaves the keys behind (verified against the
installed 1.44.19 dist - no removeItem exists there), later
frontends clean up on successful migration, and an interrupted
migration can leak the keys on any version
- comment the intentional best-effort swallow in the toast helper
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(nodes): add Easy_Utility — INT → padded STRING dispatcher
First mode `int_to_padded_string`: takes an INT widget (with up/down
arrows) and emits a zero-padded STRING (default `001`, no `v` prefix).
The downstream `koolook_versioning.resolve_version_token` prepends `v`
and re-pads, so wiring this into `EasyAIPipeline.version` /
`Easy_VideoCombine.version` yields the standard `vNNN` token at execution.
Designed for extension: the `mode` widget shipped from day one so adding
further conversions (string→int, formatting variants, etc.) doesn't
rewire saved workflows.
Note: the previously-attempted JS preview-resolver refactor that aimed
to fix the "preview shows stale version" bug was reverted in this
worktree before commit — it didn't actually fix the bug (the real cause
is the JS resolver not descending into ComfyUI subgraphs) and it broke
shot_name resolution in the process. The preview-resolver work is NOT
in this commit; only the Python node + tests.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(nodes): guard version widget against pysssss .replace crash
PR #180 added a STRING `version` widget to EasyAIPipeline and
Easy_VideoCombine. Three workflow states leave widget.value undefined
or non-string when graphToPrompt iterates widgets:
- Pre-#180 workflows replay an INT (e.g. `1`) into the new STRING slot.
- Widget-to-input conversion voids the value in newer LiteGraph.
- Easy_VideoCombine's dict-based widgets_values omits the new key
entirely on workflows saved before #180.
With pysssss/ComfyUI-Custom-Scripts installed, presetText.js patches
every STRING TextWidget.serializeValue to run callbacks that do
`value.replace(...)` for `{variable}` substitution. Any of the states
above crashes graphToPrompt and turns Run into a no-op for the whole
workflow ("Cannot read properties of undefined (reading 'replace')").
Fix: trap `widget.value` reads/writes on the version widget via
`Object.defineProperty` so the property is always coerced to a string
regardless of caller. `configurable: true` so downstream code can
still override the descriptor if it needs to.
Tests: 231 passing; both JS files parse OK.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ai-pipeline): resolve subgraph version preview
* feat(video): add path-aware load video node
* fix(ai-pipeline): preview connected text names
* fix(pr184): address review cleanup
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>