You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
In --web-bg mode (or CONDUCTOR_WEB_BG=1), the detached process is supposed to auto-shutdown after workflow completes + all clients disconnect + 30s grace. But the grace timer was only ever armed from WebSocket-disconnect code paths:
the /ws endpoint's finally block on client disconnect, and
the broadcaster's failed-send cleanup.
_on_event only set self._workflow_completed = True on a terminal event — it never armed the timer. So if no dashboard client ever connected, no disconnect fired, the timer was never started, _bg_event was never set, and wait_for_clients_disconnect() blocked forever after the workflow had already finished. The detached child became a zombie holding its port and PID file — making --web-bg unusable for headless/programmatic supervision.
Fix
_on_event now arms the grace timer on the workflow's root terminal event, gated on the existing _is_root_event(event_dict) helper. _maybe_start_grace_timer() already no-ops while clients are connected, so watched runs keep their current behavior (timer only arms once the last client disconnects).
Gating on the root event also fixes a coupled latent bug: previously _workflow_completed was set on any terminal event, including nested sub-workflow ones (which carry data.subworkflow_path). Un-gated, arming from _on_event would let a sub-workflow finishing mid-run trigger a premature shutdown.
_maybe_start_grace_timer is now loop-safe: it no-ops when there's no running event loop (a synchronous emit() in a unit test with no server) instead of leaking an orphan _grace_countdown coroutine.
Tests
Added to tests/test_web/test_server.py (TestAutoShutdown):
test_unwatched_run_arms_grace_timer_on_completion — root completion arms the timer with zero clients ever connected, and the post-run wait resolves.
test_subworkflow_completion_does_not_arm_or_set_flag — a nested sub-workflow terminal event neither sets the flag nor arms the timer; the root event still does.
Both tests fail on the pre-fix code (verified by reverting), so they're non-tautological.
In --web-bg mode the auto-shutdown grace timer was only armed from the
WebSocket-disconnect code paths. If no dashboard client ever connected,
no disconnect fired, the timer was never started, and the detached
process blocked forever in wait_for_clients_disconnect() after the
workflow had already finished, leaking its port and PID file.
Arm the timer from _on_event on the *root* workflow's terminal event,
gated on the existing _is_root_event helper so a nested sub-workflow
completion (which carries subworkflow_path) can't trigger a premature
shutdown while the root run is still executing. Also make
_maybe_start_grace_timer no-op when there is no running event loop
(synchronous emit() in tests) rather than leaking an orphan coroutine.
Adds regression tests for the unwatched-run and sub-workflow-gating
cases; both fail on the pre-fix code.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Log at debug level when _maybe_start_grace_timer's no-running-loop
guard fires, so an unexpected occurrence outside tests (which would
silently reproduce the #318 hang with zero diagnostic trail) leaves
a trace.
- Trim the duplicated rationale comment in _on_event down to the
call-site-specific "why arm here" reasoning, pointing to
_is_root_event's and _maybe_start_grace_timer's own docstrings
instead of restating them.
- Name the terminal-event condition (is_terminal_event) to avoid an
awkward line-wrapped if-statement.
- Add regression tests: workflow_failed (not just workflow_completed)
arms the timer for an unwatched run; a connected client keeps the
timer unarmed on completion (the safety-critical case protecting
watched runs from the new _on_event call site); and the existing
flag-setting tests now also assert no exception was swallowed by
WorkflowEventEmitter.emit()'s subscriber catch-all when the
loop-safety guard fires, since a bare "_grace_task is None"
assertion alone can't distinguish a guarded no-op from an unguarded
RuntimeError getting silently logged there.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
Summary
Fixes#318.
In
--web-bgmode (orCONDUCTOR_WEB_BG=1), the detached process is supposed to auto-shutdown after workflow completes + all clients disconnect + 30s grace. But the grace timer was only ever armed from WebSocket-disconnect code paths:/wsendpoint'sfinallyblock on client disconnect, and_on_eventonly setself._workflow_completed = Trueon a terminal event — it never armed the timer. So if no dashboard client ever connected, no disconnect fired, the timer was never started,_bg_eventwas never set, andwait_for_clients_disconnect()blocked forever after the workflow had already finished. The detached child became a zombie holding its port and PID file — making--web-bgunusable for headless/programmatic supervision.Fix
_on_eventnow arms the grace timer on the workflow's root terminal event, gated on the existing_is_root_event(event_dict)helper._maybe_start_grace_timer()already no-ops while clients are connected, so watched runs keep their current behavior (timer only arms once the last client disconnects)._workflow_completedwas set on any terminal event, including nested sub-workflow ones (which carrydata.subworkflow_path). Un-gated, arming from_on_eventwould let a sub-workflow finishing mid-run trigger a premature shutdown._maybe_start_grace_timeris now loop-safe: it no-ops when there's no running event loop (a synchronousemit()in a unit test with no server) instead of leaking an orphan_grace_countdowncoroutine.Tests
Added to
tests/test_web/test_server.py(TestAutoShutdown):test_unwatched_run_arms_grace_timer_on_completion— root completion arms the timer with zero clients ever connected, and the post-run wait resolves.test_subworkflow_completion_does_not_arm_or_set_flag— a nested sub-workflow terminal event neither sets the flag nor arms the timer; the root event still does.Both tests fail on the pre-fix code (verified by reverting), so they're non-tautological.
Verification
tests/test_web+tests/test_cli/test_web_flags.py)make lintcleanmake typecheckclean (the one remaining diagnostic is pre-existing inengine/dialog_evaluator.py, unrelated)No CHANGELOG / docs / frontend changes needed.