diff --git a/.beads/interactions.jsonl b/.beads/interactions.jsonl index 009803b..0104494 100644 --- a/.beads/interactions.jsonl +++ b/.beads/interactions.jsonl @@ -191,3 +191,7 @@ {"id":"int-d11b256e","kind":"field_change","created_at":"2026-08-10T02:42:16.8074426Z","actor":"G.Hirpara","issue_id":"daz-script-server-okpk","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Added AxisRemap to dazpy/math3.py: apply_vec3/apply_quat/apply_bbox with reflection rejection, Y_UP_TO_Z_UP preset, exported from dazpy package. Wired into tests.py unit runner."}} {"id":"int-27eb72b4","kind":"field_change","created_at":"2026-08-11T02:07:34.4536775Z","actor":"G.Hirpara","issue_id":"daz-script-server-063","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Won't fix, after implementation review. The already-shipped pre-check busy detection (commit 720736f, on master) already turns the original unbounded hang into a bounded, self-correcting ~15s slow-but-correct response — no data loss, no wrong result, just latency. A working bounded-wait backstop was implemented and independently verified concurrency-correct (race-free CAS ceiling, sound thread/promise lifetimes, Qt6-portable), but final review surfaced a real behavioral hazard: on timeout the queued main-thread call is NOT cancelled (Qt has no API for this) and keeps running, so the 503 the client receives is misleading (the operation is still executing), and dazpy's retry_on_busy could re-run it — a double-execution risk strictly worse than the slow-but-correct wait it replaces. The default 20s timeout also exceeds the one measured gap (~15s), so it offers little value against the observed problem and is really speculative insurance against a worse, never-observed wedged-main-thread case. Decision: the tradeoff isn't worth it as scoped; keep the existing pre-check detection only. The implementation (branch feature/studio-busy-handling, commits 680b267..467ea7b) is not merged to master. Revisit only if a genuinely unbounded hang is observed in practice, with a design that avoids the discard-and-maybe-retry hazard (e.g. never reusing a retry-safe error code for a timeout that doesn't actually stop the underlying work)."}} {"id":"int-dfa45616","kind":"field_change","created_at":"2026-08-11T02:11:53.6096817Z","actor":"G.Hirpara","issue_id":"daz-script-server-nfdr","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Docs updated (README + math3.rst), AxisRemap gained __eq__/__hash__, removed dead _matrix/_det slots (kept as locals), added 24-permutation property test covering all _mat3_to_quat branches. inverse() deliberately left as separate future item per issue note."}} +{"id":"int-fac2f1b8","kind":"field_change","created_at":"2026-08-15T02:41:37.054825Z","actor":"G.Hirpara","issue_id":"daz-script-server-3l5o","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Fixed: connected DzRenderMgr::renderFinished(bool) as a guaranteed exit path for the render busy state, since it fires on render failure even when DzScene::renderFinished(DzRenderer*) is blocked behind DAZ Studio's modal error dialog. Verified builds against both SDK4 and SDK6."}} +{"id":"int-441bfd61","kind":"field_change","created_at":"2026-08-15T03:42:33.7102845Z","actor":"G.Hirpara","issue_id":"daz-script-server-eaub","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Added AsyncRequestManager::failStaleRunning(), a 5-min-cadence watchdog that marks any request RUNNING past ASYNC_STALE_RUNNING_TIMEOUT_MIN (30 min default) as FAILED with a diagnostic message, plus a guard in markCompleted() so a late real completion can't flip an already-terminal (timed-out) request back to completed/cancelled. Live-verified on DAZ Studio 4: with timings temporarily dropped to 1 min for a fast test, a render stuck behind an Error Saving modal correctly auto-failed while the modal was still up, and stayed failed after the modal was later dismissed. Production timings (30/5 min) restored and reinstalled before commit."}} +{"id":"int-78fdac5e","kind":"field_change","created_at":"2026-08-15T03:46:42.3174912Z","actor":"G.Hirpara","issue_id":"daz-script-server-q0l4","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"cancelJson()/cancelRenderJson() now mark the request CANCELLED immediately on cancel of a RUNNING request (same as the QUEUED branch), instead of leaving status='running' when killRender() has nothing to kill. Best-effort killRenderOnMainThread() is still invoked for render requests. markCompleted()'s existing terminal-state guard (from the eaub stale-running fix) prevents a late real completion from flipping status back. Added tests/test_api.py::test_async_cancel_running_request_reaches_terminal_status (not live-verified this session — DAZ Studio wasn't running)."}} +{"id":"int-1436dca1","kind":"field_change","created_at":"2026-08-15T03:59:50.3718077Z","actor":"G.Hirpara","issue_id":"daz-script-server-j1pg","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Researched, decided not to implement. See issue notes: no public SDK hook exists to intercept the Iray render-error modal before it appears (raised by closed-source dzirayrenderer.dll, outside SDK surface); no global silent/suppress-dialogs flag on DzApp reachable from a loaded plugin; the only remaining option (a generic Qt dialog-auto-click watchdog) was rejected as too risky -- it could misfire on a legitimate interactive dialog a human is looking at, trading a bounded, well-understood degradation (already fixed by 3l5o/eaub/q0l4: 503 fail-fast + terminal status within 30 min) for an unbounded new failure mode. Recommended external process-level watchdog (restart DAZ Studio on /health timeout) if unattended headless pipelines become a real requirement."}} diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 579ff71..4411cbc 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -1,5 +1,7 @@ {"_type":"issue","id":"daz-script-server-519.2","title":"render()/render_and_wait() return type change silently breaks truthiness callers","description":"dazpy/_render.py:320 - render()/render_and_wait() changed from returning bool (master) to returning a plain RenderOutcome dataclass with no __bool__. Any caller written as 'if rs.render():' (matching the old bool contract, and this is a public SDK method other projects like vangard-daz-mcp depend on) now always evaluates truthy, even when RenderOutcome(success=False, ...) is returned, silently treating failed renders as successful.","acceptance_criteria":"Either add __bool__ to RenderOutcome that reflects success, or add a deprecation-safe path so existing truthiness checks behave correctly; document the breaking change in CHANGELOG.md.","status":"closed","priority":0,"issue_type":"bug","assignee":"G.Hirpara","owner":"ghirpara@yahoo.com","created_at":"2026-07-17T10:46:14Z","created_by":"G.Hirpara","updated_at":"2026-07-17T10:52:10Z","started_at":"2026-07-17T10:51:14Z","closed_at":"2026-07-17T10:52:10Z","close_reason":"Added RenderOutcome.__bool__ reflecting success, documented the breaking change in CHANGELOG.md, and added regression tests for truthiness checks.","dependencies":[{"issue_id":"daz-script-server-519.2","depends_on_id":"daz-script-server-519","type":"parent-child","created_at":"2026-07-17T06:46:14Z","created_by":"G.Hirpara","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"daz-script-server-519.1","title":"capture() throws ReferenceError on prevBg when backdrop_color is set","description":"dazpy/_viewport.py:221 - DazViewport.capture()'s finish_script references the JS variable prevBg, but prevBg is only declared inside the separate prepare_script execute() call (a different HTTP round trip / JS scope). set_bg/restore_bg_js were written for the old single-script version and never had prevBg threaded through the prev_state dict the way selectionName/axesOn etc. were.","acceptance_criteria":"capture(path, backdrop_color=(r,g,b)) succeeds (hide_overlays True and False) and restores the original viewport background without throwing.","status":"closed","priority":0,"issue_type":"bug","assignee":"G.Hirpara","owner":"ghirpara@yahoo.com","created_at":"2026-07-17T10:46:10Z","created_by":"G.Hirpara","updated_at":"2026-07-17T10:51:09Z","started_at":"2026-07-17T10:47:48Z","closed_at":"2026-07-17T10:51:09Z","close_reason":"Fixed prevBg round-tripping through prev_state/prep_result JSON instead of a JS var across the two execute() calls; added regression tests for both hide_overlays branches.","dependencies":[{"issue_id":"daz-script-server-519.1","depends_on_id":"daz-script-server-519","type":"parent-child","created_at":"2026-07-17T06:46:10Z","created_by":"G.Hirpara","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"daz-script-server-eaub","title":"Async render request stuck at 'running' forever on render failure (GH #34)","description":"AsyncRequestManager has no watchdog/timeout that fails a stale 'running' render request. If the render never signals completion (blocked behind DAZ Studio's modal error dialog), GET /requests/:id stays 'running' indefinitely with no way for a client to detect failure short of a very long wait. Add either: (a) a render-error signal handler that resolves the request as 'failed' with the error message, or (b) a timeout that marks it failed after a bound. Depends on daz-script-server-3l5o to correctly detect the failure condition. (GH issue #34, suggestion #2)","acceptance_criteria":"A render that errors resolves the async request to 'failed' (with a useful error message) within a bounded time, instead of staying 'running' indefinitely.","status":"closed","priority":1,"issue_type":"bug","assignee":"G.Hirpara","owner":"ghirpara@yahoo.com","created_at":"2026-08-15T02:36:13Z","created_by":"G.Hirpara","updated_at":"2026-08-15T03:42:34Z","started_at":"2026-08-15T03:06:06Z","closed_at":"2026-08-15T03:42:34Z","close_reason":"Closed","dependencies":[{"issue_id":"daz-script-server-eaub","depends_on_id":"daz-script-server-3l5o","type":"blocks","created_at":"2026-08-14T22:36:22Z","created_by":"G.Hirpara","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"daz-script-server-3l5o","title":"Render busy state never clears on failed/error render (GH #34)","description":"SceneEventBroker only wires enterBusy()/exitBusy() to aboutToRender/renderFinished (SceneEventBroker.cpp:114-116). When a render errors, DAZ Studio pops a blocking modal and renderFinished never fires, so exitBusy() is never called and the plugin's busy counter stays stuck. Every subsequent /execute then returns 503 forever, recoverable only by restarting DAZ Studio. This is the root cause and the highest-leverage fix (GH issue #34, suggestion #1).","acceptance_criteria":"A render that ends in error/abort no longer leaves the plugin permanently busy; /execute succeeds again without restarting DAZ Studio. exitBusy() fires on every render termination path, not just the success path.","notes":"Live-verified on running DAZ Studio 4 instance (2026-08-14): submitted an async render to a nonexistent drive (A:/...) via /render. Render itself succeeded (Iray converged to 97%), but the file-save step failed and DAZ Studio popped a blocking 'Error Saving' modal (Try Again / Choose New File / Cancel). While the modal was up, /execute correctly fail-fast'd with 503 STUDIO_BUSY (not a hang) and /health stayed responsive. After clicking Cancel on the modal, /execute immediately returned 200 and the async request transitioned running -\u003e completed -- no DAZ Studio restart needed. Confirms DzRenderMgr::renderFinished(bool) fires as the guaranteed exit signal even on this non-happy-path outcome.\n\nAlso live-verified on DAZ Studio 6 (2026-08-14) with the identical procedure (nonexistent-drive output_path -\u003e render succeeds, save fails, 'Error Saving' modal blocks main thread -\u003e /execute correctly 503s STUDIO_BUSY while blocked, not hung -\u003e after clicking Cancel, /execute immediately 200s and request transitions running -\u003e completed). Confirms the fix holds across both SDK4 and SDK6 builds.\n\nNote: this save-failure trick reproduces the same class of bug as the GH issue (a non-happy-path render outcome that pops a blocking modal), not the exact Iray-engine-failure repro (Scene.setBackdrop(null) -\u003e Iray DB transaction abort). Attempted that exact repro separately on both DS4 and DS6 -- it hard-crashed both, with two DIFFERENT ACCESS_VIOLATION signatures (DzBackdrop::isVisible() on DS4, DzStyledTimeAsFramesPropertyWgt::~DzStyledTimeAsFramesPropertyWgt() on DS6), confirming Scene.setBackdrop(null) is a distinct, pre-existing DAZ Studio bug unrelated to this plugin and unsafe to use for live testing (see memory feedback_dazscript_blocking_dialogs). The save-failure repro is the safe, repeatable way to exercise this exact code path.","status":"closed","priority":1,"issue_type":"bug","assignee":"G.Hirpara","owner":"ghirpara@yahoo.com","created_at":"2026-08-15T02:36:06Z","created_by":"G.Hirpara","updated_at":"2026-08-15T03:04:55Z","started_at":"2026-08-15T02:36:44Z","closed_at":"2026-08-15T02:41:37Z","close_reason":"Closed","dependency_count":0,"dependent_count":2,"comment_count":0} {"_type":"issue","id":"daz-script-server-bu86","title":"Live-verify dazpy.lighting _look_at_euler yaw sign against real DAZ Studio","description":"Final whole-branch review of daz-script-server-p1af's ThreePointLightSetup work found that _look_at_euler's yaw formula in dazpy/lighting.py is likely sign-inverted for off-axis lights (nonzero X component in the aim direction -- this includes the default rig's key and fill lights, only the rim light at azimuth=180 is unaffected). Independently re-derived via dazpy/math3.py's own _euler_to_quat XYZ rotation convention (documented to match DAZ's bone-rotation channels): the correct formula is yaw = atan2(-direction.x, -direction.z), not the current atan2(direction.x, -direction.z). This derivation is solid but has NOT been empirically confirmed against a live DAZ Studio session, because no server was reachable during review (generic node rotation-control composition order was assumed to match the bone convention, but not independently confirmed for DzSpotLight/DzDistantLight/DzPointLight). Per user decision, the sign flip was deliberately withheld pending live verification rather than applied blind.\n\nAction: with DAZ Studio running, create a rig via apply_three_point_light_setup at a known azimuth (e.g. 90) against a known target, then call DazLight.direction() on the resulting light and confirm it points at the target. If direction() confirms the current code's -90 deg is wrong and +90 is correct, apply the one-line fix (dazpy/lighting.py, the yaw calculation in _look_at_euler) and update test_look_at_euler_yaws_toward_90_azimuth_offset's expected value from -90.0 to +90.0 in tests/test_dazpy.py. If the live check instead confirms the current sign is correct, remove the caveat docstring note and close this without a code change.","status":"closed","priority":1,"issue_type":"bug","owner":"ghirpara@yahoo.com","created_at":"2026-08-07T12:19:05Z","created_by":"G.Hirpara","updated_at":"2026-08-08T02:22:27Z","closed_at":"2026-08-08T02:22:27Z","close_reason":"Confirmed live against running DAZ Studio: distant light rotated to y=+90 reports DazLight.direction()=(-1,0,~0), matching the intended convention; prior y=-90 was inverted. Fix applied and committed to feature/dazpy-lighting (commit f26e571). Full suite 557/557 passing.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"daz-script-server-fsq8","title":"Reintroduce face-region pass for LoRA-intensity control (no identity conditioning)","description":"Live-tuning against jason_a/abby_b (daz-script-server-r5br) found a new artifact after 3os's rebuild: the Graphic_Novel style LoRA at full strength (0.8) draws harsh 'pencil mark' ink lines on facial features (brow ridge, under-eye, under-nose) that read as artifactual rather than stylistic, confirmed by isolating lora_strength=0.0 (marks vanish entirely) vs 0.8 (marks present) with all ControlNet/composite settings held constant -- ruled out Canny threshold, composite opacity, and controlnet_lineart_weight as causes first (all had negligible effect). Lowering lora_strength globally (tested 0.3-0.6) fixes the face but also strips most of the body's ink-hatching texture, since the current architecture (post-3os) is a single full-image pass -- there's no way to apply a different LoRA intensity to the face vs body.\n\nRoot cause: faces read 'harsher' than bodies at any single style intensity (high-frequency geometric detail vs. smooth clothing), the same tension that originally motivated the FaceDetailer/IPAdapterFaceID branch this epic's rebuild (3os) removed. That removal correctly fixed toe's specific artifacts (traced to IPAdapterFaceID/SEGSDetailer's identity-conditioning behavior at denoise=0.35/faceid_weight=1.0), but didn't address the underlying face/body intensity mismatch -- it just removed the only mechanism that let the two differ.\n\nFix: reintroduce a face-region crop+resample pass, reusing the parts of the old mechanism that were never implicated in toe (face bbox/SAM detection via UltralyticsDetectorProvider+SAMDetectorCombined, and the per-region ControlNet chain via ImpactControlNetApplyAdvancedSEGS -- both confirmed innocent by toe's zero-baseline test, which disabled the whole face-refinement chain and still reproduced the artifacts). Explicitly exclude IPAdapterFaceID/IPAdapterInsightFaceLoader/IPAdapterUnifiedLoaderFaceID entirely -- no identity conditioning of any kind. New mechanism: a second LoraLoader patched at a lower strength than the main pass, feeding SEGSDetailer for the face crop resample, so face and body get independently tunable LoRA intensity.","design":"workflow_controlnet.json: add back UltralyticsDetectorProvider, BboxDetectorSEGS, SAMLoader, SAMDetectorCombined, MaskToSEGS, 3x ImpactControlNetApplyAdvancedSEGS (normal/depth/lineart), SEGSDetailer, SEGSPaste -- same node shapes as the pre-3os workflow (see git show 04b6274~1:docs/examples/rendering/sprite_matrix/workflow_controlnet.json for reference), but with a NEW second LoraLoader node (patched at face_pass_lora_strength) feeding SEGSDetailer's basic_pipe instead of IPAdapterFaceID's output. No IPAdapter/InsightFace nodes at all.\n\nNew config.py fields on ComfyUIStageConfig: face_pass_enabled (bool), face_pass_lora_strength (float), face_pass_denoise (float), face_pass_guide_size (float), face_pass_bbox_dilation (int), face_pass_controlnet_normal_weight/depth_weight/lineart_weight (float) -- reuse the old face_detailer_* naming pattern minus faceid_weight (no identity conditioning to tune). Thread through schema.py, render_shot.py CLI, stylize_stage.py, workflow_builder.py following the same pattern as the removed face_detailer_* fields.\n\nLive-tune face_pass_lora_strength against jason_a and abby_b once wired up -- 0.5 looked promising in ad-hoc testing during r5br but wasn't tested through the actual crop/paste mechanism, only as a global override.","acceptance_criteria":"Face renders without harsh pencil-mark lines (brow ridge, under-eye, under-nose) on both jason_a and abby_b. Body retains full ink-hatching texture (main pass LoRA strength unchanged from whatever was chosen for body). No identity-conditioning artifacts reintroduced (toe stays fixed) -- verify since this does bring back SEGSDetailer, which was part of the original toe-implicated chain even though the root cause was specifically IPAdapterFaceID/faceid conditioning, not SEGSDetailer itself.","status":"closed","priority":1,"issue_type":"feature","assignee":"G.Hirpara","owner":"ghirpara@yahoo.com","created_at":"2026-08-07T01:12:05Z","created_by":"G.Hirpara","updated_at":"2026-08-07T01:53:13Z","started_at":"2026-08-07T01:12:10Z","closed_at":"2026-08-07T01:53:13Z","close_reason":"Abandoned in favor of a simpler fix. Root cause was correctly diagnosed (Graphic_Novel LoRA draws harsh ink lines on facial creases at full strength) but the proposed face-region-pass fix backfired when tested: reusing the old per-region ControlNet weights (1.4/1.1/0.9, tuned for a different context with identity-locked low stylization) fought against the new lower-strength face LoRA and produced worse artifacts (heavy dark under-eye blocking), unresponsive to further LoRA reduction. Superseded by daz-script-server-r5br's negative-prompt suppression fix, which resolves the same root cause with zero architecture change. workflow_controlnet.json/workflow_builder.py changes from this issue were reverted.","dependencies":[{"issue_id":"daz-script-server-fsq8","depends_on_id":"daz-script-server-gbx","type":"parent-child","created_at":"2026-08-06T21:12:09Z","created_by":"G.Hirpara","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"daz-script-server-r5br","title":"Live-tune lineart_composite_opacity and validate toe/a5o resolved","description":"Part of daz-script-server-3os. Once the rebuild is wired up (daz-script-server-7f2, -4a0, -5do, -pxmn), live-test against abby_b and jason_a: confirm daz-script-server-toe's artifacts (pale/ghostlike skin, harsh eye-shadow split) are gone; confirm facial identity is still recognizable without IPAdapterFaceID conditioning (the accepted trade-off of this rebuild); confirm daz-script-server-a5o (hair color drift) is resolved as a side effect; tune lineart_composite_opacity if full-strength (1.0) reads as over-inked/noisy on skin. Close toe and a5o once confirmed, or reopen/adjust design if identity loss is unacceptable.","acceptance_criteria":"Live-verified against both abby_b and jason_a. Final lineart_composite_opacity value chosen and recorded in config.py comments. toe and a5o closed with reference to this validation, or a documented follow-up filed if identity conditioning needs to be reintroduced some other way.","notes":"CORRECTION after initial close: 120/240 Canny thresholds still left small isolated stray marks (forehead, eyebrow tip, under-lip) on jason_a, visible at high zoom -- caught by user review of the copied final images. Isolated the residual cause (now that the LoRA's own line-drawing was already suppressed): tiny high-contrast points in the Iray render (specular highlights, skin pores) still exceeding even 120/240 in isolated spots. Tested 150/300, 200/400, and bilateral-filter pre-smoothing against a clean (negative-prompt-fixed) baseline -- all three cleared the residual marks equally well, so picked 150/300 as the more conservative change with no bilateral-filter complexity needed. Final default is 150/300, not 120/240. Re-verified through the actual production CLI (render_shot.py) at 3x zoom on both jason_a and abby_b -- clean.","status":"closed","priority":1,"issue_type":"task","owner":"ghirpara@yahoo.com","created_at":"2026-08-07T00:11:21Z","created_by":"G.Hirpara","updated_at":"2026-08-07T02:01:48Z","closed_at":"2026-08-07T01:53:14Z","close_reason":"Final fix has two parts, both live-verified against jason_a and abby_b through the actual production pipeline (render_shot.py):\n1. Negative-prompt suppression (config.py ComfyUIStageConfig.negative_prompt): added terms against harsh facial crosshatching/under-eye ink/exaggerated brow lines. Root cause: the Graphic_Novel LoRA at full strength (0.8) draws ink strokes along facial creases as a learned stylistic behavior (confirmed via isolating lora_strength=0.0 vs 0.8 with all else held constant) -- promptable away without any architecture change or LoRA/body-hatching tradeoff.\n2. Canny threshold retune (canvas_convert.derive_lineart default 80/160 -\u003e 120/240, now also exposed as config/CLI knobs -- see daz-script-server-017n): removes the residual contribution from real Iray shading-edge lines (brow ridge, under-nose) picked up by the deterministic lineart composite pass, independent of the LoRA behavior.\nBoth were needed together -- prompt suppression alone left a faint under-nose/brow line from the composite step; threshold retune alone (tested earlier) didn't fully clear the LoRA-drawn lines.\nlineart_composite_opacity stays at 1.0 (no longer needs lowering now that both upstream sources are addressed).\nA face-region-pass approach (daz-script-server-fsq8) was attempted and abandoned -- see that issue's close reason.","dependencies":[{"issue_id":"daz-script-server-r5br","depends_on_id":"daz-script-server-3os","type":"parent-child","created_at":"2026-08-06T20:11:28Z","created_by":"G.Hirpara","metadata":"{}"},{"issue_id":"daz-script-server-r5br","depends_on_id":"daz-script-server-5do","type":"blocks","created_at":"2026-08-06T20:11:30Z","created_by":"G.Hirpara","metadata":"{}"},{"issue_id":"daz-script-server-r5br","depends_on_id":"daz-script-server-pxmn","type":"blocks","created_at":"2026-08-06T20:11:30Z","created_by":"G.Hirpara","metadata":"{}"}],"dependency_count":2,"dependent_count":0,"comment_count":0} @@ -54,6 +56,7 @@ {"_type":"issue","id":"daz-script-server-70f","title":"daz-bridge-usd: new plugin project scaffold","description":"Create the daz-bridge-usd C++ DAZ Studio plugin project. Set up CMake build system, add DazBridgeUtils as a git submodule, configure DAZ SDK linkage, and establish the plugin entry point.\\n\\nThis is the foundation all other daz-bridge-usd tasks build on. The plugin should follow the same build conventions as daz-script-server (.env for SDK path, build.sh, MSVC /MD runtime).","status":"closed","priority":1,"issue_type":"task","assignee":"G.Hirpara","owner":"ghirpara@yahoo.com","created_at":"2026-05-22T21:53:26Z","created_by":"G.Hirpara","updated_at":"2026-05-22T22:06:00Z","started_at":"2026-05-22T22:02:50Z","closed_at":"2026-05-22T22:06:00Z","close_reason":"Scaffold complete: CMakeLists.txt, build.sh, pluginmain.cpp, common_version.h, dzbridgeusd.h in y:/working/BlueMoonFoundry/daz-bridge-usd. Builds clean to DazBridgeUsd.dll. DzBridgeUsdAction stub registered; implementation deferred to daz-script-server-5xu.","dependency_count":0,"dependent_count":1,"comment_count":0} {"_type":"issue","id":"daz-script-server-cu2","title":"Route registration interface in daz-script-server","description":"Add a plugin route registration API to daz-script-server so that companion plugins (like daz-bridge-usd) can register their own HTTP handlers on the same port and httplib instance without modifying the server plugin directly.\\n\\nThe server plugin should expose a Qt-accessible registration method (e.g. registerRouteHandler(method, path, handler)) that other DAZ plugins loaded in the same process can call at startup via DAZ's plugin discovery. This keeps the HTTP surface unified on one port while keeping plugin code cleanly separated.","notes":"Implemented registerPluginRoute(method, path, receiver, slotName) and unregisterPluginRoute(method, path) as Q_INVOKABLE methods on DzScriptServerPane. Routes stored in m_pluginRoutes list (QMutex-protected), applied via applyPluginRoutes() at end of setupRoutes() each time the server starts. Companion plugin handler slot must be Q_INVOKABLE with signature HttpResult(QByteArray body, QByteArray clientIP). Uses QPointer\u003cQObject\u003e in the httplib lambda to safely handle plugin unload. Build clean. Routes take effect at next server start — companion plugins register during DAZ startup before the server starts, so timing is correct.","status":"closed","priority":1,"issue_type":"feature","assignee":"G.Hirpara","owner":"ghirpara@yahoo.com","created_at":"2026-05-22T21:53:23Z","created_by":"G.Hirpara","updated_at":"2026-05-22T22:01:08Z","started_at":"2026-05-22T21:56:58Z","closed_at":"2026-05-22T22:01:08Z","close_reason":"Closed","dependency_count":0,"dependent_count":1,"comment_count":0} {"_type":"issue","id":"daz-script-server-dou","title":"Add POST /render endpoint (single render, async)","description":"VN production workflows need to trigger DAZ renders programmatically. Without a render endpoint, every render requires manual GUI interaction, making batch sprite production impossible to automate.\n\nImplement POST /render that accepts a render job, enqueues it through the existing AsyncRequestManager, executes on the main thread, and returns a request_id immediately.\n\nRequest body schema:\n- output_path (required, absolute path)\n- width / height (optional, override scene render settings)\n- format: png|jpg|tiff|exr (optional, inferred from output_path extension)\n- camera (optional, null = active camera)\n- engine (optional, null = scene default: iray|3delight|filament)\n- figure / morphs (optional, single-figure morph dict)\n- figures (optional, list of {name, morphs} for multi-figure scenes)\n- iray_samples (optional quality override)\n\nRender jobs use id prefix 'rnd-' and flow through the existing processNextAsyncRequest() path as a special request type. The server generates a DazScript snippet from the morph dict, applies morphs via findPropertyByLabel(), then calls App.getRenderMgr().render(opts). Unknown morph names are silently skipped (non-fatal). Status/result polling via existing /requests/:id/status and /requests/:id/result endpoints.\n\nOpen questions to resolve during implementation:\n1. morph reset between variants (reset_morphs flag needed?)\n2. Figure lookup fallback: error vs fall back to primary figure\n3. Add isRender flag to AsyncRequest struct in AsyncRequestManager.h","design":"1. reset_morphs flag: Add reset_morphs: bool to POST /render request body. When true, reset all figure morphs to zero on the main thread before applying the morphs dict and rendering. Required for correct variant rendering when the same figure is reused across batch jobs.\n\n2. Figure lookup failure = error: On figure lookup failure, return an HTTP error (not a silent fallback). Before any render step begins, run a validation pass over the entire figures array to identify all figure references and validate they resolve to scene nodes. This surfaces all bad references at once rather than failing mid-render.\n\n3. RequestType enum instead of isRender bool: Add RequestType enum (REQUEST_TYPE_SCRIPT, REQUEST_TYPE_RENDER) to AsyncRequest struct rather than a boolean isRender flag. Thread it through submit() or a new submitRender() overload. Used in two places: (a) cancelJson() — only invoke killRenderOnMainThread() when cancelling a render request, not any running request; (b) processNextAsyncRequest() — gate render-progress signal wiring for future SSE progress endpoint (owd).","notes":"Implementation complete and tested (2026-05-21). Build clean, installed, verified via live DAZ Studio instance.\n\nAPI corrections confirmed against running DAZ Studio (dzirayrenderer.dll):\n- Image size: new QSize(w, h) — NOT Qt.size() (Qt global undefined in DAZ QScriptEngine)\n- Engine switching: renderMgr.findRenderer(className) + setActiveRenderer() confirmed working. Iray className = 'DzIrayRenderer'. 3Delight/Filament class names follow Dz*Renderer pattern but need verification on systems with those plugins.\n- Camera: Scene.findCameraByLabel() + App.getViewportMgr().setActiveCamera() confirmed.\n- Render: renderMgr.doRender(opts) confirmed.\n- Output path: opts.renderImgFilename + opts.renderImgToId = 2 (DirectToFile) confirmed.\n\nKnown remaining TODO:\n- iray_samples: NOT on DzRenderOptions or DzIrayRenderer object in DazScript. DAZ stores iray-specific settings outside the standard render options API. Field is accepted but silently ignored. Needs further investigation — scene data items path not yet found. Resume investigation in a future session.","status":"closed","priority":1,"issue_type":"feature","assignee":"G.Hirpara","owner":"ghirpara@yahoo.com","created_at":"2026-05-21T20:39:38Z","created_by":"G.Hirpara","updated_at":"2026-05-22T21:56:48Z","started_at":"2026-05-22T01:58:32Z","closed_at":"2026-05-22T21:56:48Z","close_reason":"Closed","dependency_count":0,"dependent_count":3,"comment_count":0} +{"_type":"issue","id":"daz-script-server-q0l4","title":"POST /render/:id/cancel is a no-op on in-flight renders and misreports status (GH #34)","description":"AsyncRequestManager::cancelRenderJson() (AsyncRequestManager.cpp:259-314) only sets req.status = REQUEST_CANCELLED when the request is still REQUEST_QUEUED. If the request is already running, it just sets cancelRequested=1 and invokes killRenderOnMainThread() -- but the HTTP response unconditionally returns {\"status\":\"cancelled\"} regardless of branch, while the tracker's internal status is left as 'running'. If killRender() finds nothing to kill (e.g. renderer already wedged behind a modal), the request never leaves 'running'. Same bug exists in the non-render cancelJson() path. (GH issue #34, suggestion #3)","acceptance_criteria":"After POST /render/:id/cancel (or /requests/:id/cancel) on a running request, GET /requests/:id reflects a terminal status (cancelled or failed) consistent with the cancel response, even when killRender() has nothing to kill.","notes":"Live-verified 2026-08-14 against running DAZ Studio 4: submitted a busy-loop script (500M iterations, ~8s, non-render), confirmed it reached RUNNING via poll, then DELETE /requests/:id returned {status:cancelled} and an immediate follow-up GET /requests/:id/status also showed cancelled (not stuck at running) -- the underlying loop kept executing in the background as expected (non-render scripts have no kill mechanism), but the tracker no longer misreports it. Regression test tests/test_api.py::test_async_cancel_running_request_reaches_terminal_status confirmed passing standalone and as part of the cancel/async group.","status":"closed","priority":2,"issue_type":"bug","assignee":"G.Hirpara","owner":"ghirpara@yahoo.com","created_at":"2026-08-15T02:36:16Z","created_by":"G.Hirpara","updated_at":"2026-08-15T03:52:20Z","started_at":"2026-08-15T03:44:49Z","closed_at":"2026-08-15T03:46:42Z","close_reason":"cancelJson()/cancelRenderJson() now mark the request CANCELLED immediately on cancel of a RUNNING request (same as the QUEUED branch), instead of leaving status='running' when killRender() has nothing to kill. Best-effort killRenderOnMainThread() is still invoked for render requests. markCompleted()'s existing terminal-state guard (from the eaub stale-running fix) prevents a late real completion from flipping status back. Added tests/test_api.py::test_async_cancel_running_request_reaches_terminal_status (not live-verified this session — DAZ Studio wasn't running).","dependencies":[{"issue_id":"daz-script-server-q0l4","depends_on_id":"daz-script-server-3l5o","type":"blocks","created_at":"2026-08-14T22:36:23Z","created_by":"G.Hirpara","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"daz-script-server-017n","title":"Expose Canny lineart thresholds as tunable config (canny_low_threshold/canny_high_threshold)","description":"Live-tuning against jason_a (part of daz-script-server-r5br) found the default Canny thresholds in derive_lineart() (canvas_convert.py, low_threshold=80/high_threshold=160) pick up soft Iray shading/skin-crease edges on the face (eyebrow ridge, under-nose, under-eye) that read as harsh 'pencil marks' at full composite opacity -- confirmed via a grid test: raising to 120/240 removed the harsh face lines while preserving body clothing-hatching and the toe fix, at lineart_composite_opacity=1.0. 160/280 gave no further improvement over 120/240. Add canny_low_threshold/canny_high_threshold fields to ComfyUIStageConfig (config.py), thread through schema.py parsing, render_shot.py CLI flags, and stylize_stage.py's derive_lineart() call, following the exact pattern lineart_composite_opacity was added in daz-script-server-pxmn/5do. Default to the live-verified 120/240 pair rather than the untuned 80/160.","acceptance_criteria":"canny_low_threshold/canny_high_threshold configurable via spec JSON and render_shot.py CLI, defaulting to 120/240. derive_lineart() call sites updated. Live-verified the new defaults still look correct on at least jason_a (already done informally during r5br; formal re-check optional).","status":"closed","priority":2,"issue_type":"task","owner":"ghirpara@yahoo.com","created_at":"2026-08-07T00:35:09Z","created_by":"G.Hirpara","updated_at":"2026-08-07T01:53:12Z","closed_at":"2026-08-07T01:53:12Z","close_reason":"Implemented: canny_low_threshold/canny_high_threshold added to config.py (default 120/240), threaded through schema.py, render_shot.py CLI, stylize_stage.py. Live-verified against jason_a and abby_b.","dependencies":[{"issue_id":"daz-script-server-017n","depends_on_id":"daz-script-server-3os","type":"parent-child","created_at":"2026-08-06T20:35:13Z","created_by":"G.Hirpara","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"daz-script-server-q7b","title":"Add async DazClient (dazpy.aio) using httpx","description":"dazpy's DazClient is fully synchronous (requests-style HTTP), forcing async frameworks (FastAPI, FastMCP, ComfyUI, asyncio/Temporal) to wrap every call in asyncio.to_thread() or write custom httpx.AsyncClient wrappers. Add an AsyncDazClient in a new dazpy.aio module, mirroring the sync API surface (execute_script, execute_file, batch ops, etc.) via httpx.AsyncClient.","status":"closed","priority":2,"issue_type":"feature","assignee":"G.Hirpara","owner":"ghirpara@yahoo.com","created_at":"2026-08-06T23:57:52Z","created_by":"G.Hirpara","updated_at":"2026-08-07T02:21:53Z","started_at":"2026-08-07T02:14:01Z","closed_at":"2026-08-07T02:21:53Z","close_reason":"Added dazpy.aio.AsyncDazClient (httpx.AsyncClient-backed), mirroring DazClient's full method surface as async def methods, with tests in tests/test_dazpy_aio.py (21 passing incl. real-httpx-transport and SSE-parsing checks via respx).","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"daz-script-server-8aa","title":"ComfyUI result timeout too tight under VRAM contention","description":"Live testing found a real timeout: with Daz Studio's Iray session concurrently holding ~13.6GB VRAM, ComfyUI fell back to slow 'dynamic VRAM loading' model paging and a single stylize prompt took 513s -- past the old 300s client-side timeout in stylize_stage.py/render_shot.py's comfy.save_result() calls, so the pipeline reported a valid ComfyUI result as a client-side failure.","acceptance_criteria":"Timeout raised to 600s in both stylize_stage.py and render_shot.py; verified live that a shot completes well within the new timeout once VRAM contention is relieved.","status":"closed","priority":2,"issue_type":"bug","owner":"ghirpara@yahoo.com","created_at":"2026-08-06T11:35:14Z","created_by":"G.Hirpara","updated_at":"2026-08-06T11:35:14Z","closed_at":"2026-08-06T11:35:14Z","close_reason":"Fixed (commit 5b4bf9e): timeout raised 300s-\u003e600s in both stylize_stage.py and render_shot.py. Verified live: the same jason_a back-camera shot that previously timed out at 513s completed in under 9s once VRAM contention (a concurrent Daz Studio Iray session) was relieved.","dependency_count":0,"dependent_count":0,"comment_count":0} @@ -139,6 +142,7 @@ {"_type":"issue","id":"daz-script-server-0dt","title":"JSON event serialization for all event types","description":"Implement serializeEvent() in SceneEventBroker for each signal category.\n\nOutput format per event:\n {\"type\":\"node.added\",\"ts\":1716307200123,\"data\":{...}}\n\nEvent types and their data payloads:\n- scene.loading, scene.loaded, scene.saving, scene.saved(filename), scene.clear_starting, scene.cleared\n- node.added(node_id, node_name, node_type), node.removed(node_id, node_name), node.list_changed\n- skeleton.added(node_id, node_name), skeleton.removed(node_id, node_name)\n- light.added(node_id, node_name, node_type), light.removed(node_id, node_name)\n- camera.added(node_id, node_name), camera.removed(node_id, node_name)\n- selection.list_changed, selection.primary_changed(node_id, node_name) -- empty data{} if null\n- time.changed(time_value, fps), playback.started, playback.finished\n- render.started, render.finished\n\nUse existing JsonBuilder for safe serialization. Extract node_id via node-\u003egetElementID() or similar DAZ SDK call -- verify exact method name against SDK headers.","status":"closed","priority":2,"issue_type":"task","owner":"ghirpara@yahoo.com","created_at":"2026-05-21T17:49:26Z","created_by":"G.Hirpara","updated_at":"2026-05-21T17:55:35Z","closed_at":"2026-05-21T17:55:35Z","close_reason":"Implemented in SceneEventBroker.cpp: makeEvent(), nodeInfoJson(), per-slot JsonBuilder calls for all event types","dependencies":[{"issue_id":"daz-script-server-0dt","depends_on_id":"daz-script-server-2bs","type":"parent-child","created_at":"2026-05-21T13:50:14Z","created_by":"G.Hirpara","metadata":"{}"},{"issue_id":"daz-script-server-0dt","depends_on_id":"daz-script-server-azx","type":"blocks","created_at":"2026-05-21T13:49:44Z","created_by":"G.Hirpara","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} {"_type":"issue","id":"daz-script-server-isb","title":"SceneEventBroker and SubscriberQueue classes","description":"Create include/SceneEventBroker.h and src/SceneEventBroker.cpp.\n\nSceneEventBroker (QObject, main thread):\n- Connects to dzApp-\u003egetScene() signals via Qt::DirectConnection on start()\n- Disconnects all signals on stop()\n- Maintains QMutex-protected list of SubscriberQueue*\n- registerSubscriber(queue, filter_mask) and unregisterSubscriber(queue)\n- Internal: serializeEvent(type, data) -\u003e QString JSON\n\nSubscriberQueue (one per SSE client):\n- QMutex + QWaitCondition + std::deque\u003cQString\u003e\n- push(const QString\u0026 event) -- called from main thread\n- pop(int timeout_ms) -\u003e QString -- called from HTTP thread, blocks\n- close() -- signals the queue is done (server stopping)\n\nFilter mask: bitmask of event categories (node, selection, scene, time, render, light, camera, skeleton).","status":"closed","priority":2,"issue_type":"task","assignee":"G.Hirpara","owner":"ghirpara@yahoo.com","created_at":"2026-05-21T17:49:16Z","created_by":"G.Hirpara","updated_at":"2026-05-21T17:55:36Z","started_at":"2026-05-21T17:50:54Z","closed_at":"2026-05-21T17:55:36Z","close_reason":"SceneEventBroker + SubscriberQueue in include/SceneEventBroker.h and src/SceneEventBroker.cpp","dependencies":[{"issue_id":"daz-script-server-isb","depends_on_id":"daz-script-server-0dt","type":"blocks","created_at":"2026-05-21T13:49:25Z","created_by":"G.Hirpara","metadata":"{}"},{"issue_id":"daz-script-server-isb","depends_on_id":"daz-script-server-2bs","type":"parent-child","created_at":"2026-05-21T13:50:14Z","created_by":"G.Hirpara","metadata":"{}"},{"issue_id":"daz-script-server-isb","depends_on_id":"daz-script-server-8rd","type":"blocks","created_at":"2026-05-21T13:49:34Z","created_by":"G.Hirpara","metadata":"{}"}],"dependency_count":2,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"daz-script-server-2bs","title":"Scene Change Callbacks via SSE","description":"Implement real-time scene change notifications over Server-Sent Events (SSE). Clients connect to GET /scene/events and receive push notifications when the DAZ scene changes. Replaces the need for polling. See plan in conversation history for full architecture.","status":"closed","priority":2,"issue_type":"epic","owner":"ghirpara@yahoo.com","created_at":"2026-05-21T17:49:08Z","created_by":"G.Hirpara","updated_at":"2026-05-21T17:58:30Z","closed_at":"2026-05-21T17:58:30Z","close_reason":"Scene change callbacks via SSE fully implemented: SceneEventBroker + SubscriberQueue + GET /scene/events endpoint with auth, filter, debounce, keepalive, and lifecycle wiring","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"daz-script-server-j1pg","title":"Investigate suppressing/handling modal error dialogs during server-driven renders (GH #34)","description":"DAZ Studio pops a blocking 'Error during rendering!' modal on Iray failures, which wedges the main thread and is undismissable in a headless/server-driven workflow. Reporter notes DAZ's headless mode is brittle (some plugins fail to load, preventing Studio from starting). Investigate whether a render-error signal exists that can be hooked before the modal appears, or another way to suppress/auto-dismiss the dialog while a server-driven render is in flight. Optional/exploratory relative to daz-script-server-3l5o and daz-script-server-... which address the plugin-side symptoms; this addresses the root modal itself. (GH issue #34, suggestion #4)","acceptance_criteria":"Documented findings on whether the modal can be suppressed or auto-dismissed for server-driven renders, and a decision on whether to implement it.","notes":"RESEARCH FINDINGS (2026-08-15), no code changes:\n\n1. No pre-modal hook exists in the public DAZ Studio 4.5+ SDK. Checked\n dzrendermgr.h (DzRenderMgr: aboutToRender/renderFinished(bool) only),\n dzrenderer.h (DzRenderer: aboutToRender/renderFinished(DzRenderer*),\n killRender(); DiErrorHandler is a RenderMan-style C callback for shader\n compilation errors, not the Iray render-failure path), and\n dzrenderhandler.h (DzRenderHandler: killRender() signal only). None of\n these fire *before* the \"Error during rendering!\" modal -- they're the\n same signals daz-script-server-3l5o already wired up, which only let us\n react to the busy-state symptom after the fact, not prevent the modal.\n\n2. The modal itself is raised by dzirayrenderer.dll (confirmed present at\n .../DAZStudio4/plugins/dzirayrenderer.dll), a closed-source renderer\n plugin outside the public SDK surface entirely. There's no vtable,\n signal, or setting this project can hook to intercept it before display.\n\n3. No global \"silent\"/\"suppress dialogs\" flag exists on DzApp (checked\n dzapp.h in full) that a loaded plugin could toggle at runtime.\n DzApp::GraphicsMode::NoInterface is a construction-time argument to the\n whole application (decided by DAZ Studio's own main(), before any plugin\n loads) -- not reachable from inside a plugin, and per the reporter's own\n note in the original GH #34 report, headless mode is independently\n brittle (some plugins fail to load in it), so it's not a viable target\n even if it were reachable.\n\n4. The remaining option is a generic Qt-level watchdog: a QTimer scanning\n QApplication::topLevelWidgets() for an active modal QDialog/QMessageBox\n and synthesizing a click on its default button, scoped to fire only\n while a server-initiated render is in flight (a state we already track).\n Technically feasible, but explicitly NOT recommended:\n - Requires matching on dialog title/text (\"Error during rendering!\"),\n which is unversioned, unlocalized-fragile, and could silently break\n or misfire across DAZ Studio releases.\n - No way to scope it to *only* the Iray-error dialog without also\n risking auto-dismissing a legitimate interactive dialog a human user\n is looking at in the same DAZ Studio session (this plugin runs\n alongside normal interactive use, not just headless server use) --\n a false-positive click could discard real work (e.g. an unrelated\n save-conflict prompt) with no way for the user to notice.\n - Attempting to reproduce the exact GH #34 repro (Scene.setBackdrop\n (null) -\u003e Iray DB transaction abort) for live testing was already\n tried during daz-script-server-3l5o and hard-crashed DAZ Studio with\n two different ACCESS_VIOLATION signatures on DS4 and DS6 (see that\n issue's notes, and memory feedback_dazscript_blocking_dialogs) --\n this exact failure mode is inherently unsafe to even test against\n reliably, let alone build automated dialog-clicking logic around.\n\nDECISION: Do not implement auto-dismiss/suppression. The three already-\nclosed GH #34 fixes (3l5o busy-state clearing, eaub stale-running\nwatchdog, q0l4 cancel status) already bound the *API-visible* symptom --\n/execute fails fast with 503 while a modal blocks the main thread, and\nasync requests resolve to a terminal status within\nASYNC_STALE_RUNNING_TIMEOUT_MIN (30 min) instead of polling \"running\"\nforever. A blind dialog-clicking watchdog would trade a well-understood,\nbounded degradation for an unbounded new failure mode (misclicking a\nreal dialog) to fix a cosmetic remainder (the human still has to\neventually dismiss the modal or restart DAZ Studio to fully unstick the\nmain thread). Recommend closing as won't-implement; if unattended/\nheadless render pipelines become a real requirement later, the safer\nanswer is an external process-level watchdog that restarts DAZ Studio\nwhen the plugin's /health stops responding, not in-process dialog\nsuppression.","status":"closed","priority":3,"issue_type":"task","assignee":"G.Hirpara","owner":"ghirpara@yahoo.com","created_at":"2026-08-15T02:36:20Z","created_by":"G.Hirpara","updated_at":"2026-08-15T03:59:50Z","started_at":"2026-08-15T03:56:25Z","closed_at":"2026-08-15T03:59:50Z","close_reason":"Researched, decided not to implement. See issue notes: no public SDK hook exists to intercept the Iray render-error modal before it appears (raised by closed-source dzirayrenderer.dll, outside SDK surface); no global silent/suppress-dialogs flag on DzApp reachable from a loaded plugin; the only remaining option (a generic Qt dialog-auto-click watchdog) was rejected as too risky -- it could misfire on a legitimate interactive dialog a human is looking at, trading a bounded, well-understood degradation (already fixed by 3l5o/eaub/q0l4: 503 fail-fast + terminal status within 30 min) for an unbounded new failure mode. Recommended external process-level watchdog (restart DAZ Studio on /health timeout) if unattended headless pipelines become a real requirement.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"daz-script-server-v6sk","title":"dazpy.cinematics: CinematicAnimatedShot (real interpolated keyframes)","description":"dazpy.cinematics currently only supports static/per-frame camera placement (CinematicStaticShot, OrbitCamera, FrameSubject — shipped, see docs/superpowers/specs/2026-08-11-dazpy-cinematics-design.md). GitHub issue #31 also proposed a general CinematicShot concept; the static slice deliberately deferred true interpolated animation. This issue covers CinematicAnimatedShot: a camera move with real DazScript keyframes (addKey or equivalent) rather than per-frame setValue writes. Blocked on confirming DAZ Studio's keyframe/addKey DazScript API against a live instance (unconfirmed as of 2026-08-11) -- same category of research gap as HDRIEnvironment was before it shipped.","status":"open","priority":3,"issue_type":"feature","owner":"ghirpara@yahoo.com","created_at":"2026-08-11T11:23:23Z","created_by":"G.Hirpara","updated_at":"2026-08-11T11:23:23Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"daz-script-server-jnbm","title":"Verify getRenderElementObjects()[3] Environment holder assumption via readback","description":"apply_hdri_environment() and DazRenderSettings' environment setters (_render.py) assume App.getRenderMgr().getRenderElementObjects()[3] is always the Iray Environment Options holder. This was confirmed against exactly one live DAZ Studio 4.x instance during design research for daz-script-server-x6sy. If that index is wrong for a different DAZ Studio version, or reordered, or on the SDK6/Qt6 build this repo also supports, every environment write silently no-ops (matches the existing if (!holder) return; / if (p) ... pattern already used by _set_iray_property) and apply_hdri_environment() returns None as if it succeeded -- the caller has no way to know the HDRI was never applied.\n\nFlagged by the final whole-branch review of the HDRIEnvironment branch (Important #2, non-blocking) as a real gap worth a follow-up rather than a merge blocker, since it's an inherited pattern from pre-existing code (_set_iray_property has the same silent-no-op shape) rather than a new regression.\n\nSuggested fix: add a post-apply readback in apply_hdri_environment (e.g. re-read 'Environment Intensity' via the already-present but currently test-only DazRenderSettings._get_environment_property and compare against what was just set), raising a clear error on mismatch -- mirroring the existing precedent in DazRenderSettings.set_render_engine, whose docstring notes it 'returns only after exact readback' and raises RenderError on disagreement.","status":"closed","priority":3,"issue_type":"task","assignee":"G.Hirpara","owner":"ghirpara@yahoo.com","created_at":"2026-08-08T03:22:50Z","created_by":"G.Hirpara","updated_at":"2026-08-10T01:46:46Z","started_at":"2026-08-10T01:44:20Z","closed_at":"2026-08-10T01:46:46Z","close_reason":"Added post-apply readback verification (Environment Intensity) in apply_hdri_environment; raises RenderError on holder-unavailable or mismatch instead of silently no-oping","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"daz-script-server-x6sy","title":"Research and implement dazpy.lighting HDRIEnvironment (IBL/dome lighting)","description":"GitHub issue #31 proposed dazpy.lighting.HDRIEnvironment alongside ThreePointLightSetup and SetLightColor. ThreePointLightSetup shipped (daz-script-server-p1af); SetLightColor needed no new code (already covered by DazLight.set_color). HDRIEnvironment was explicitly deferred during design (docs/superpowers/specs/2026-08-07-dazpy-lighting-design.md) because no dome/environment-map DazScript primitive currently exists in dazpy -- this needs research into what DAZ Studio actually exposes for Iray environment/IBL lighting (DzUberEnvironmentShader or similar) before a design can be written.","status":"closed","priority":3,"issue_type":"feature","owner":"ghirpara@yahoo.com","created_at":"2026-08-07T12:19:13Z","created_by":"G.Hirpara","updated_at":"2026-08-10T01:49:50Z","closed_at":"2026-08-10T01:49:50Z","close_reason":"Shipped: design spec + plan (docs/superpowers/specs|plans/2026-08-07-dazpy-hdri-environment*.md), HDRIEnvironment dataclass, apply_hdri_environment, validated environment-map setter, package exports, and readback verification (daz-script-server-jnbm) — all landed across commits 691d04c..b62ca4c","dependency_count":0,"dependent_count":0,"comment_count":0} diff --git a/README.md b/README.md index 0f42cbc..f70ba5d 100644 --- a/README.md +++ b/README.md @@ -724,6 +724,8 @@ Long-running operations (renders, exports, batch jobs) no longer need to block t **TTL:** Completed, failed, and cancelled requests are automatically purged after 1 hour (cleanup timer fires every 5 minutes). +**Stale running requests:** If a request has been `running` for more than 30 minutes without completing — e.g. a render blocked behind a DAZ Studio modal dialog — it's automatically marked `failed` so polling clients aren't left waiting forever. The underlying DAZ Studio operation may still be stuck; check the DAZ Studio window directly if this happens. + --- ## What's New in v1.2.0 diff --git a/include/AsyncRequestManager.h b/include/AsyncRequestManager.h index 00851c5..4297628 100644 --- a/include/AsyncRequestManager.h +++ b/include/AsyncRequestManager.h @@ -106,6 +106,16 @@ class AsyncRequestManager { // Called from cleanup timer on the main thread. Returns count removed. int cleanupExpired(qint64 ttlMs = 60LL * 60LL * 1000LL); + // Mark any request stuck in RUNNING for longer than staleMs as FAILED. + // Covers the case where the underlying DazScript call (e.g. a render + // that ends up blocked behind a DAZ Studio modal dialog) never returns: + // without this, GET /requests/:id would report "running" forever. This + // only updates the client-visible status -- it cannot un-block the main + // thread, so if the blocked call eventually does return, markCompleted() + // is a no-op for a request already in this terminal state (see markCompleted). + // Called from cleanup timer on the main thread. Returns count marked failed. + int failStaleRunning(qint64 staleMs); + // Mark all QUEUED and RUNNING requests as CANCELLED. Call on server stop // so that poll results after restart show the correct terminal state. void cancelAllPending(const QString& reason = "Server stopped"); diff --git a/include/DzScriptServerPane.h b/include/DzScriptServerPane.h index ddfa27b..e96c57a 100644 --- a/include/DzScriptServerPane.h +++ b/include/DzScriptServerPane.h @@ -74,6 +74,10 @@ namespace ServerConfig { // Async request management const int ASYNC_CLEANUP_INTERVAL_MIN = 5; // How often to purge TTL-expired async requests + const int ASYNC_STALE_RUNNING_TIMEOUT_MIN = 30; // A RUNNING request older than this is presumed + // blocked (e.g. behind a DAZ Studio modal) and + // reported as FAILED so it doesn't poll as + // "running" forever. See failStaleRunning(). } class DzScript; diff --git a/include/SceneEventBroker.h b/include/SceneEventBroker.h index e695d2d..07aa9c7 100644 --- a/include/SceneEventBroker.h +++ b/include/SceneEventBroker.h @@ -16,6 +16,7 @@ class DzSkeleton; class DzLight; class DzCamera; class DzRenderer; +class DzRenderMgr; // ─── Event filter categories ────────────────────────────────────────────────── // Bitmask values passed to SubscriberQueue to control which event categories @@ -181,6 +182,13 @@ private slots: // Render void onAboutToRender(DzRenderer* r); void onRenderFinished(DzRenderer* r); + // DzRenderMgr's renderFinished(bool) is the authoritative render-lifecycle + // signal: unlike DzScene::renderFinished(DzRenderer*), it fires even when + // the render errors out (DAZ Studio pops a blocking modal dialog in that + // case, which can prevent DzScene::renderFinished from ever being emitted). + // Whichever "finished" signal arrives first clears the render busy state; + // see m_renderBusy. + void onRenderMgrFinished(bool succeeded); private: void dispatch(int categoryBit, const QString& event); @@ -194,6 +202,7 @@ private slots: QTimer* m_pSelectionDebounce; // 50 ms single-shot, debounces selection bursts DzTime m_pendingTime; bool m_started; + bool m_renderBusy; // guards against double enterBusy()/exitBusy() from the two render-finished signals mutable QAtomicInt m_busyDepth; // count of active nested busy operations; written on main thread only mutable QAtomicInt m_busyReason; // MainThreadBusy::Reason of the outermost active operation }; diff --git a/src/AsyncRequestManager.cpp b/src/AsyncRequestManager.cpp index 9574623..81ae6a1 100644 --- a/src/AsyncRequestManager.cpp +++ b/src/AsyncRequestManager.cpp @@ -233,12 +233,20 @@ std::pair AsyncRequestManager::cancelJson( if (id != qid) newQueue.enqueue(id); } m_queue = newQueue; - req.status = REQUEST_CANCELLED; - req.error = "Cancelled by client"; - req.completedAt = QDateTime::currentMSecsSinceEpoch(); } else { needKillRender = (req.requestType == REQUEST_TYPE_RENDER); } + + // Mark cancelled immediately regardless of whether we can actually + // interrupt a RUNNING request's underlying operation (killRender() + // may find nothing to kill, e.g. a renderer already wedged behind a + // modal). The tracker's status must match the "cancelled" response + // we're about to return, not silently stay "running" forever. If the + // underlying call does complete later, markCompleted() no-ops for a + // request already in a terminal state (see markCompleted). + req.status = REQUEST_CANCELLED; + req.error = "Cancelled by client"; + req.completedAt = QDateTime::currentMSecsSinceEpoch(); } if (needKillRender) { @@ -290,12 +298,18 @@ std::pair AsyncRequestManager::cancelRenderJson( if (id != qid) newQueue.enqueue(id); } m_queue = newQueue; - req.status = REQUEST_CANCELLED; - req.error = "Cancelled by client"; - req.completedAt = QDateTime::currentMSecsSinceEpoch(); } else { needKillRender = true; } + + // See the matching comment in cancelJson(): mark cancelled + // immediately even though killRender() is best-effort and may find + // nothing to kill (renderer already wedged behind a modal), so the + // tracker's status can't be left stuck at "running" forever while + // the HTTP response already claims "cancelled". + req.status = REQUEST_CANCELLED; + req.error = "Cancelled by client"; + req.completedAt = QDateTime::currentMSecsSinceEpoch(); } if (needKillRender) { @@ -408,6 +422,13 @@ void AsyncRequestManager::markCompleted(const QString& id, bool executed, if (!m_requests.contains(id)) return; AsyncRequest& req = m_requests[id]; + + // If failStaleRunning() already timed this request out (RUNNING -> FAILED) + // while the underlying DazScript call was blocked, that terminal state + // sticks -- a late, real completion must not flip it back to + // COMPLETED/CANCELLED after a client may have already acted on "failed". + if (req.status != REQUEST_RUNNING) return; + req.completedAt = QDateTime::currentMSecsSinceEpoch(); req.progress = 1.0; req.outputLines = output; @@ -471,6 +492,30 @@ int AsyncRequestManager::cleanupExpired(qint64 ttlMs) return toRemove.size(); } +int AsyncRequestManager::failStaleRunning(qint64 staleMs) +{ + qint64 now = QDateTime::currentMSecsSinceEpoch(); + int failedCount = 0; + + QMutexLocker locker(&m_mutex); + for (QMap::iterator it = m_requests.begin(); + it != m_requests.end(); ++it) { + AsyncRequest& req = it.value(); + if (req.status == REQUEST_RUNNING && req.startedAt > 0 && + (now - req.startedAt) > staleMs) { + req.status = REQUEST_FAILED; + req.error = "Request timed out after running for longer than expected; " + "DAZ Studio's main thread may be blocked behind a dialog " + "(e.g. a failed render's error prompt). This request's script " + "may still complete in the background, but its result will no " + "longer be tracked -- check DAZ Studio directly."; + req.completedAt = now; + ++failedCount; + } + } + return failedCount; +} + void AsyncRequestManager::cancelAllPending(const QString& reason) { QMutexLocker locker(&m_mutex); diff --git a/src/DzScriptServerPane.cpp b/src/DzScriptServerPane.cpp index b96f7b3..255f3de 100644 --- a/src/DzScriptServerPane.cpp +++ b/src/DzScriptServerPane.cpp @@ -2897,7 +2897,9 @@ void DzScriptServerPane::processNextAsyncRequest() QMetaObject::invokeMethod(this, "processNextAsyncRequest", Qt::QueuedConnection); } -// Removes completed/failed/cancelled requests older than 1 hour. +// Removes completed/failed/cancelled requests older than 1 hour, and fails +// out any request that has been RUNNING for longer than +// ServerConfig::ASYNC_STALE_RUNNING_TIMEOUT_MIN (see failStaleRunning()). // Fired by m_pCleanupTimer every 5 minutes on the main thread. void DzScriptServerPane::cleanupExpiredRequests() { @@ -2905,6 +2907,13 @@ void DzScriptServerPane::cleanupExpiredRequests() if (removed > 0) { appendLog(QString("[INFO] Async cleanup: removed %1 expired request(s)").arg(removed)); } + + int failed = m_pAsyncMgr->failStaleRunning( + (qint64)ServerConfig::ASYNC_STALE_RUNNING_TIMEOUT_MIN * 60 * 1000); + if (failed > 0) { + appendLog(QString("[WARN] Async cleanup: %1 request(s) stuck running past %2 min, marked failed") + .arg(failed).arg(ServerConfig::ASYNC_STALE_RUNNING_TIMEOUT_MIN)); + } } // Called on the main thread via killRenderRequested signal from AsyncRequestManager. diff --git a/src/SceneEventBroker.cpp b/src/SceneEventBroker.cpp index 5285ce5..c418efd 100644 --- a/src/SceneEventBroker.cpp +++ b/src/SceneEventBroker.cpp @@ -7,6 +7,8 @@ #include #include #include +#include +#include #include @@ -55,6 +57,7 @@ SceneEventBroker::SceneEventBroker(QObject* parent) , m_pSelectionDebounce(nullptr) , m_pendingTime(0) , m_started(false) + , m_renderBusy(false) , m_busyDepth(0) , m_busyReason(MainThreadBusy::Idle) { @@ -115,6 +118,16 @@ void SceneEventBroker::start() { connect(scene, SIGNAL(aboutToRender(DzRenderer*)), this, SLOT(onAboutToRender(DzRenderer*))); connect(scene, SIGNAL(renderFinished(DzRenderer*)), this, SLOT(onRenderFinished(DzRenderer*))); + // DzRenderMgr::renderFinished(bool) is connected as a second, guaranteed + // exit path: DzScene::renderFinished(DzRenderer*) is not emitted when a + // render errors out and DAZ Studio blocks the main thread behind its + // modal "Error during rendering!" dialog, which otherwise leaves the + // plugin's busy state (and therefore /execute) stuck forever. + DzRenderMgr* renderMgr = dzApp ? dzApp->getRenderMgr() : nullptr; + if (renderMgr) { + connect(renderMgr, SIGNAL(renderFinished(bool)), this, SLOT(onRenderMgrFinished(bool))); + } + m_started = true; } @@ -125,6 +138,11 @@ void SceneEventBroker::stop() { disconnect(dzScene, nullptr, this, nullptr); } + DzRenderMgr* renderMgr = dzApp ? dzApp->getRenderMgr() : nullptr; + if (renderMgr) { + disconnect(renderMgr, nullptr, this, nullptr); + } + m_pTimeDebounce->stop(); m_pSelectionDebounce->stop(); m_started = false; @@ -310,13 +328,32 @@ void SceneEventBroker::onPlaybackFinished() { // ─── Render ─────────────────────────────────────────────────────────────────── void SceneEventBroker::onAboutToRender(DzRenderer* /*r*/) { - enterBusy(MainThreadBusy::Rendering); + if (!m_renderBusy) { + enterBusy(MainThreadBusy::Rendering); + m_renderBusy = true; + } dispatch(SceneEventFilter::Render, makeEvent("render.started", "{}")); } void SceneEventBroker::onRenderFinished(DzRenderer* /*r*/) { - exitBusy(); + if (m_renderBusy) { + exitBusy(); + m_renderBusy = false; + } dispatch(SceneEventFilter::Render, makeEvent("render.finished", "{}")); } +// Guaranteed exit path — see the comment in start() where this is connected. +void SceneEventBroker::onRenderMgrFinished(bool succeeded) { + if (m_renderBusy) { + exitBusy(); + m_renderBusy = false; + } + JsonBuilder j; + j.startObject(); + j.addMember("succeeded", succeeded); + j.finishObject(); + dispatch(SceneEventFilter::Render, makeEvent("render.finished", j.toString())); +} + #include "moc_SceneEventBroker.cpp" diff --git a/tests/test_api.py b/tests/test_api.py index 1c535c9..6ccac8c 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -395,6 +395,44 @@ def test_async_cancel_queued_request(self): if cancel_r.status_code == 200: self.assertEqual(cancel_r.json().get("status"), "cancelled") + def test_async_cancel_running_request_reaches_terminal_status(self): + # A busy-loop script long enough to still be RUNNING when we cancel it. + # killRender()/killRenderOnMainThread() has nothing to kill for a plain + # script (not a render), so this exercises the case where the cancel + # response says "cancelled" but the underlying call can't actually be + # interrupted -- the tracker must still report a terminal status + # instead of leaving GET /requests/:id stuck at "running" (GH #34). + # ~8s on a live DAZ Studio 4 instance -- long enough to reliably catch + # "running" below, short enough not to starve the main thread for + # other tests once cancelled (the loop itself can't be interrupted, + # only the tracker's reported status can). + script = iife("var x = 0; for (var i = 0; i < 500000000; i++) { x += i; } return x;") + r = async_execute(script=script) + request_id = r.json()["request_id"] + + # Wait for it to actually start running before cancelling. + deadline = time.time() + 5 + status = None + while time.time() < deadline: + status_r = requests.get(f"{BASE_URL}/requests/{request_id}/status", + headers=auth_headers(), timeout=5) + status = status_r.json().get("status") + if status == "running": + break + time.sleep(0.05) + self.assertEqual(status, "running", "request never reached running state") + + cancel_r = requests.delete(f"{BASE_URL}/requests/{request_id}", + headers=auth_headers(), timeout=5) + self.assertEqual(cancel_r.status_code, 200) + self.assertEqual(cancel_r.json().get("status"), "cancelled") + + # GET status must not stay "running" forever -- it should already + # reflect the terminal status the cancel response promised. + status_r = requests.get(f"{BASE_URL}/requests/{request_id}/status", + headers=auth_headers(), timeout=5) + self.assertEqual(status_r.json().get("status"), "cancelled") + def test_async_cancel_already_finished_returns_400(self): r = async_execute(script=iife("return 1;")) request_id = r.json()["request_id"]