fix(engine): add cross-process merge guard to prevent concurrent merges

Multiple engine processes (dashboard + serve) share the same SQLite database
but each has its own in-memory merge queue. Without a cross-process check,
two processes can start merging different tasks simultaneously.

Added store.getActiveMergingTask() as a DB-level check before any merge
starts. The drainMergeQueue defers with pollIntervalMs delay, and both
aiMergeTask and processPullRequestMergeTask have safety-net checks.
Also moved stale merge status cleanup to run regardless of autoMerge setting.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-04-13 20:21:58 -07:00
parent 827c0370c3
commit f9f7aff3ec
7 changed files with 70 additions and 3 deletions

View File

@@ -1435,6 +1435,24 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
return sorted.slice(offset, offset + Math.max(0, limit));
}
/**
* Returns the ID of a task currently in an active merge status ("merging" or
* "merging-pr"), optionally excluding a specific task ID.
*
* This is a lightweight database-level check used as a cross-process guard:
* multiple engine processes share the same SQLite database, but each has its
* own in-memory merge queue. Without this check, two processes can start
* merging different tasks simultaneously.
*/
getActiveMergingTask(excludeTaskId?: string): string | undefined {
const sql = excludeTaskId
? `SELECT id FROM tasks WHERE status IN ('merging', 'merging-pr') AND id != ? LIMIT 1`
: `SELECT id FROM tasks WHERE status IN ('merging', 'merging-pr') LIMIT 1`;
const params = excludeTaskId ? [excludeTaskId] : [];
const row = this.db.prepare(sql).get(...params) as { id: string } | undefined;
return row?.id;
}
/**
* Search tasks by full-text query across title, ID, description, and comments.
* Uses SQLite FTS5 for fast tokenized matching with relevance ranking.