Restructure duoworkflow runner around session, lease and stop coordinator

What does this MR do and why?

This MR restructures workhorse/internal/ai_assist/duoworkflow ahead of detached execution and parallel actions. It builds on the lifecycle fixes from !263064 (merged), which is merged.

Each commit builds and passes tests on its own, so reviewing commit by commit is easiest:

  1. teardownTracker and stopCoordinator get their own types and files (teardown.go, stop_coordinator.go). This moves about 150 lines out of runner.go. Concurrent stopWorkflow callers (keepalive, client reader, Shutdown) now send one StopWorkflowRequest and share its acknowledgment.

  2. workflowLease (in lock.go) holds the lock manager, the Redis mutex and the workflow ID. Before, the runner kept these as five separate fields. The lease acquires the lock on the start request, releases it at most once, and does nothing when locking is disabled. A later MR can make the lock mandatory or renew it without touching the runner.

  3. One session per run (session.go), shared by the runner, the stream manager, the HTTP action handler and the MCP round tripper. It holds:

    • the original request
    • the run's context, derived from the request and cancelled when the runner closes
    • the precomputed X-Forwarded-For value, which was duplicated in actions.go and mcp.go
    • the workflow ID behind a lock

    This removes the unsynchronized httpActionHandler.workflowID and the separate MCP workflowState. Log lines from the run now carry workflow_id. Detached execution only has to derive the session context differently.

  4. Only the first StartWorkflowRequest on a connection is accepted. Before, a second one tried to take the lock again, replaced the workflow ID that other goroutines read, and appended the MCP tools a second time.

  5. A failed stop send is reported to every caller, instead of later callers waiting out the stop timeout.

  6. Tests: each hand-built runner shares one session.

Behaviour changes, beyond the restructuring:

  • Second start request: a connection that sends a second StartWorkflowRequest now ends with an error. When locking was enabled, this already ended the run with a lock conflict (1013); now it gets the default close. The web chat sends one start request per socket and reconnects to start again.
  • Stop reason: DWS receives the first stop reason only. A Workhorse shutdown that follows a client disconnect no longer sends a second request with WORKHORSE_SERVER_SHUTDOWN.

References

Screenshots or screen recordings

Not applicable: no UI changes.

How to set up and validate locally

  1. Open Agentic Chat in the web UI and send a message that triggers tool calls.
  2. In the Workhorse logs, confirm the duoworkflow lines for the run carry workflow_id after the start request.
  3. Close the tab mid-turn. Confirm a single stopWorkflow: sending stop workflow request... line, and that the workhorse:duoworkflow:lock:<workflow_id> Redis key is removed.
  4. Reopen the chat and confirm it resumes normally.

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

Edited by Igor Drozdov

Merge request reports

Loading
Loading