Summary
The concurrent-run guard in executor.ts can be bypassed when two workflow runs are launched nearly simultaneously on the same cwd. Pre-created rows have status='pending' at the time the guard checks for 'running'/'paused', so both runs pass the guard.
Root Cause
The sequence:
Run A: orchestrator pre-creates row (status='pending')
Run B: orchestrator pre-creates row (status='pending')
Run A: executeWorkflow → getActiveWorkflowRunByPath → sees no 'running' row → passes guard
Run B: executeWorkflow → getActiveWorkflowRunByPath → sees no 'running' row → passes guard
Run A: updateWorkflowRun(status='running')
Run B: updateWorkflowRun(status='running')
-- Both runs now executing concurrently on the same cwd --
The guard at packages/workflows/src/executor.ts:324 queries:
SELECT * FROM remote_agent_workflow_runs
WHERE working_path = $1 AND status IN ('running', 'paused')
ORDER BY started_at DESC LIMIT 1
But the row is inserted with status='pending' at packages/core/src/orchestrator/orchestrator.ts:346, and only transitions to 'running' later at executor.ts:535.
Impact
- Currently mitigated by worktree isolation — each run gets a unique
cwd, so the guard condition (same working_path) is never hit in practice.
- Would manifest if two workflows are run on the same repo with
--no-worktree, or if worktree creation fails and falls back to the source directory.
- When two runs share a
cwd, bash/script nodes writing to relative paths can cross-contaminate.
Possible Fixes
- Include
'pending' in the guard query's status filter
- Set status to
'running' at pre-creation time instead of 'pending'
- Use a database-level advisory lock or
INSERT ... ON CONFLICT to serialize the guard check + status update atomically
Files
packages/workflows/src/executor.ts:324 — guard query
packages/workflows/src/executor.ts:535 — status transition to 'running'
packages/core/src/orchestrator/orchestrator.ts:346 — pre-creation with 'pending'
packages/core/src/db/workflows.ts:190-192 — getActiveWorkflowRunByPath SQL
Discovered During
Investigation of concurrent workflow runs (#995 investigation side-quest). Confirmed no data corruption occurred — worktree isolation prevented the race from manifesting.
Summary
The concurrent-run guard in
executor.tscan be bypassed when two workflow runs are launched nearly simultaneously on the samecwd. Pre-created rows havestatus='pending'at the time the guard checks for'running'/'paused', so both runs pass the guard.Root Cause
The sequence:
The guard at
packages/workflows/src/executor.ts:324queries:But the row is inserted with
status='pending'atpackages/core/src/orchestrator/orchestrator.ts:346, and only transitions to'running'later atexecutor.ts:535.Impact
cwd, so the guard condition (sameworking_path) is never hit in practice.--no-worktree, or if worktree creation fails and falls back to the source directory.cwd, bash/script nodes writing to relative paths can cross-contaminate.Possible Fixes
'pending'in the guard query's status filter'running'at pre-creation time instead of'pending'INSERT ... ON CONFLICTto serialize the guard check + status update atomicallyFiles
packages/workflows/src/executor.ts:324— guard querypackages/workflows/src/executor.ts:535— status transition to'running'packages/core/src/orchestrator/orchestrator.ts:346— pre-creation with'pending'packages/core/src/db/workflows.ts:190-192—getActiveWorkflowRunByPathSQLDiscovered During
Investigation of concurrent workflow runs (#995 investigation side-quest). Confirmed no data corruption occurred — worktree isolation prevented the race from manifesting.