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:
-
teardownTrackerandstopCoordinatorget their own types and files (teardown.go,stop_coordinator.go). This moves about 150 lines out ofrunner.go. ConcurrentstopWorkflowcallers (keepalive, client reader,Shutdown) now send oneStopWorkflowRequestand share its acknowledgment. -
workflowLease(inlock.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. -
One
sessionper 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-Forvalue, which was duplicated inactions.goandmcp.go - the workflow ID behind a lock
This removes the unsynchronized
httpActionHandler.workflowIDand the separate MCPworkflowState. Log lines from the run now carryworkflow_id. Detached execution only has to derive the session context differently. -
Only the first
StartWorkflowRequeston 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. -
A failed stop send is reported to every caller, instead of later callers waiting out the stop timeout.
-
Tests: each hand-built runner shares one session.
Behaviour changes, beyond the restructuring:
- Second start request: a connection that sends a second
StartWorkflowRequestnow 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
- Audit and plan: https://gitlab.com/gitlab-org/gitlab/-/work_items/634419
- Builds on (merged): !263064 (merged)
- Prepares for:
Screenshots or screen recordings
Not applicable: no UI changes.
How to set up and validate locally
- Open Agentic Chat in the web UI and send a message that triggers tool calls.
- In the Workhorse logs, confirm the
duoworkflowlines for the run carryworkflow_idafter the start request. - Close the tab mid-turn. Confirm a single
stopWorkflow: sending stop workflow request...line, and that theworkhorse:duoworkflow:lock:<workflow_id>Redis key is removed. - 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.