Skip to content

--web-bg reports success and prints a URL for workflows that never start #410

Description

Problem

conductor run --web-bg prints a dashboard URL and exits successfully even when the workflow never actually starts. The child process dies moments later and the URL points at a dashboard that is already gone.

Reproduced with a workflow that fails config validation:

❯ conductor run ship@conductor-workflows --web-bg --input issue=404
Dashboard: http://127.0.0.1:41234

…while the same workflow in the foreground fails immediately:

❯ conductor run ship@conductor-workflows --input issue=404
Loading workflow: /home/jason/src/conductor-workflows/workflows/ship/workflow.yaml
╭─────────────── ❌ ConfigurationError ───────────────╮
│  Configuration validation failed in                 │
│  '.../workflows/ship/workflow.yaml':                │
╰─────────────────────────────────────────────────────╯

Root cause

Two independent gaps:

1. The dashboard binds the port before the workflow config is loaded.

In cli/run.py::run_workflow_async, await dashboard.start() happens before load_config(workflow_path):

if web:
    dashboard = WebDashboard(...)
    await dashboard.start()      # <-- port is now listening

try:
    verbose_log(f"Loading workflow: {workflow_path}")
    config = load_config(workflow_path)   # <-- ConfigurationError raised here

Note that resume_workflow_async in the same file already does the opposite — it constructs the dashboard early but defers dashboard.start() until after load_config and the replay seeding, so the run path is also inconsistent with resume.

2. The readiness probe only checks that a TCP port is open.

cli/bg_runner.py::_wait_for_server opens a socket to 127.0.0.1:<port> and returns True as soon as anything accepts. Combined with (1), "the dashboard is listening" is satisfied while the workflow has not been loaded, let alone started. _finalize_background_launch then writes the PID file and launch_background returns a BackgroundLaunch, so the parent reports success.

_wait_for_server also never checks proc.poll() inside its loop, so when the child does die before binding, the parent still burns the full 15-second timeout before reporting anything.

Proposed fix

Confirm the workflow is actually running — not merely that a socket is open — before --web-bg returns:

  1. Add a workflow-start probe. GET /api/info on the dashboard already returns {} until a workflow_started event lands, and a populated {run_id, workflow_name, started_at, ...} once it does — a ready-made readiness signal with no new endpoint. After the port probe succeeds, poll it until workflow_started is observed.
  2. Watch for early child exit throughout. Check proc.poll() on every iteration of both the port wait and the new workflow-start wait, and fail immediately with the exit code instead of waiting out the timeout.
  3. Surface the actual error. Include a tail of the captured child stderr log in the RuntimeError message so the user sees the ConfigurationError inline rather than having to open $TMPDIR/conductor/conductor-<name>-<ts>-<runid>.bg.stderr.log.
  4. Bound the wait sensibly. Startup legitimately includes plugin source prefetch (git clone), MCP server construction, and provider connection validation, so the workflow-start probe needs a generous timeout (~30s, ideally env-overridable). If the deadline passes but the child is still alive, proceed and report the URL with a "still initializing" note rather than failing — the goal is to catch dead children, not to penalise slow ones.
  5. Move dashboard.start() after load_config in run_workflow_async, matching resume_workflow_async. This makes config errors fail before the port is ever bound, and is a parity fix in its own right.

A failure that happens after workflow_started (a provider error, a failing first agent) should still return the URL — the dashboard genuinely renders that failure and is useful. The bug is specifically about failures before the workflow ever starts.

Notes for implementation

  • BackgroundLaunch would need a field (e.g. workflow_started: bool) so cli/app.py can print the "still initializing" note in the timeout case.
  • Roughly 20 test sites across tests/test_cli/test_bg_runner.py, test_web_flags.py, test_resume_command.py, and tests/test_config/test_instructions.py patch conductor.cli.bg_runner._wait_for_server directly; they will need the new probe patched alongside it, otherwise they will run the real probe against a non-listening port and wait out the timeout.
  • Both launch_background and launch_background_resume share _spawn_bg_child_finalize_background_launch, so the fix lands in one place and applies to run and resume alike.

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions