fix(postgres): scope the cross-process merge guard to the project (#2267)

## fix(postgres): scope the cross-process merge guard to the project

The guard's own comment (`project-engine.ts`) says it checks whether
another process is merging a task **"for this project"** — and in SQLite
mode the per-project DB file made that scoping implicit.
`getActiveMergingTaskImpl`'s `backendMode` branch queries the shared PG
`tasks` table with **no `project_id` filter**, so one merging task
anywhere serializes merges across **all** projects.

### Production evidence
6-project embedded-PG deployment: **697 cross-project `Merge deferred …
is already merging (cross-process guard)` retries in 10 minutes** — six
independent repos waiting on each other's serialized merger, collapsing
merge throughput ~6x and letting `in-review` pile up to 95 tasks.

### Fix
Add the existing `taskProjectScope(layer)` filter to the query's
conditions (one line + import). It is a no-op when the layer carries no
`projectId`, so single-project deployments and the SQLite path are
unchanged. Same pattern as the other project-scoped task queries.

Deployed on the affected instance: cross-project merges now proceed in
parallel; per-project serialization (the guard's documented intent) is
preserved.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Improved merge task handling so activity in one project no longer
unnecessarily blocks merge operations in other projects.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: TrinaryCompute <fusion-merge@trinarycompute.dev>
This commit is contained in:
TrinaryCompute
2026-07-17 00:46:41 -06:00
committed by GitHub
parent 7760d783bd
commit bf6a974b58

View File

@@ -13,6 +13,7 @@ import { BUILTIN_WORKFLOW_SETTINGS } from "../builtin-workflow-settings.js";
import { isBuiltinWorkflowId } from "../builtin-workflows.js";
import { fromJson } from "../db.js";
import * as schema from "../postgres/schema/index.js";
import { taskProjectScope } from "../postgres/data-layer.js";
import { ensureBranchGroupForSource as ensureBranchGroupForSourceAsync, ensurePrEntityForSource as ensurePrEntityForSourceAsync, getActivePrEntityBySource as getActivePrEntityBySourceAsync, getBranchGroup as getBranchGroupAsync, getBranchGroupByBranchName as getBranchGroupByBranchNameAsync, getBranchGroupBySource as getBranchGroupBySourceAsync, getPrEntity as getPrEntityAsync, getPrThreadState as getPrThreadStateAsync, listActivePrEntities as listActivePrEntitiesAsync, listBranchGroups as listBranchGroupsAsync, listPrThreadStates as listPrThreadStatesAsync, recordPrThreadOutcome as recordPrThreadOutcomeAsync } from "./async-branch-groups.js";
import { getWorkflowWorkItem as getWorkflowWorkItemAsync } from "./async-workflow-workitems.js";
import { type TaskRow } from "./persistence.js";
@@ -635,6 +636,18 @@ export async function getActiveMergingTaskImpl(store: TaskStore, excludeTaskId?:
isNull(schema.project.tasks.deletedAt),
inArray(schema.project.tasks.status, ["merging", "merging-pr"]),
];
/*
* FNXC:PostgresProjectIsolation 2026-07-17-00:00:
* The cross-process merge guard is documented (project-engine.ts) as
* "another process is already merging a task for THIS project" — in
* SQLite mode the per-project DB file made that scoping implicit. The
* shared PG tasks table needs the explicit project_id filter; without it
* one merging task anywhere serializes merges across ALL projects
* (observed: 697 cross-project "Merge deferred" retries in 10 minutes on
* a 6-project deployment, collapsing merge throughput ~6x).
*/
const scope = taskProjectScope(layer);
if (scope) conditions.push(scope);
if (excludeTaskId) {
conditions.push(ne(schema.project.tasks.id, excludeTaskId));
}