mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-01 15:35:12 +01:00
Fix blocker readiness in reused workspaces
This commit is contained in:
1 parent
568f1d3517
commit
6de173d35c
3 files changed
+438
-14
No files matched your search
@@ -2592,6 +2592,7 @@ describeEmbeddedPostgres("issueService blockers and dependency wake readiness",
|
||||
await db.delete(activityLog);
|
||||
await db.delete(issues);
|
||||
await db.delete(workspaceOperations);
|
||||
await db.delete(heartbeatRuns);
|
||||
await db.delete(executionWorkspaces);
|
||||
await db.delete(projectWorkspaces);
|
||||
await db.delete(projects);
|
||||
@@ -2643,6 +2644,105 @@ describeEmbeddedPostgres("issueService blockers and dependency wake readiness",
|
||||
expect(blockedRelations.blockedBy.map((relation) => relation.id)).toEqual([blockerId]);
|
||||
});
|
||||
|
||||
it("ignores malformed cross-company blocker relations in summaries, readiness, and dependent wakes", async () => {
|
||||
const companyA = randomUUID();
|
||||
const companyB = randomUUID();
|
||||
const localBlockerId = randomUUID();
|
||||
const localBlockedId = randomUUID();
|
||||
const foreignBlockerId = randomUUID();
|
||||
const foreignDependentId = randomUUID();
|
||||
const foreignAssigneeAgentId = randomUUID();
|
||||
|
||||
await db.insert(companies).values([
|
||||
{
|
||||
id: companyA,
|
||||
name: "Paperclip A",
|
||||
issuePrefix: `A${companyA.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
},
|
||||
{
|
||||
id: companyB,
|
||||
name: "Paperclip B",
|
||||
issuePrefix: `B${companyB.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
},
|
||||
]);
|
||||
|
||||
await db.insert(agents).values({
|
||||
id: foreignAssigneeAgentId,
|
||||
companyId: companyB,
|
||||
name: "Foreign assignee",
|
||||
role: "engineer",
|
||||
status: "active",
|
||||
adapterType: "codex_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {},
|
||||
permissions: {},
|
||||
});
|
||||
|
||||
await db.insert(issues).values([
|
||||
{
|
||||
id: localBlockerId,
|
||||
companyId: companyA,
|
||||
title: "Local blocker",
|
||||
status: "done",
|
||||
priority: "medium",
|
||||
},
|
||||
{
|
||||
id: localBlockedId,
|
||||
companyId: companyA,
|
||||
title: "Local blocked issue",
|
||||
status: "blocked",
|
||||
priority: "medium",
|
||||
},
|
||||
{
|
||||
id: foreignBlockerId,
|
||||
companyId: companyB,
|
||||
title: "Foreign blocker",
|
||||
status: "todo",
|
||||
priority: "medium",
|
||||
},
|
||||
{
|
||||
id: foreignDependentId,
|
||||
companyId: companyB,
|
||||
title: "Foreign dependent",
|
||||
status: "blocked",
|
||||
priority: "medium",
|
||||
assigneeAgentId: foreignAssigneeAgentId,
|
||||
},
|
||||
]);
|
||||
|
||||
await db.insert(issueRelations).values([
|
||||
{
|
||||
companyId: companyA,
|
||||
issueId: foreignBlockerId,
|
||||
relatedIssueId: localBlockedId,
|
||||
type: "blocks",
|
||||
},
|
||||
{
|
||||
companyId: companyA,
|
||||
issueId: localBlockerId,
|
||||
relatedIssueId: foreignDependentId,
|
||||
type: "blocks",
|
||||
},
|
||||
]);
|
||||
|
||||
await expect(svc.getDependencyReadiness(localBlockedId)).resolves.toMatchObject({
|
||||
blockerIssueIds: [],
|
||||
unresolvedBlockerIssueIds: [],
|
||||
isDependencyReady: true,
|
||||
});
|
||||
await expect(svc.getRelationSummaries(localBlockedId)).resolves.toMatchObject({
|
||||
blockedBy: [],
|
||||
blocks: [],
|
||||
});
|
||||
await expect(svc.getRelationSummaries(localBlockerId)).resolves.toMatchObject({
|
||||
blockedBy: [],
|
||||
blocks: [],
|
||||
});
|
||||
await expect(svc.listWakeableBlockedDependents(localBlockerId)).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it("adds terminal blockers to immediate blocked-by summaries", async () => {
|
||||
const companyId = randomUUID();
|
||||
await db.insert(companies).values({
|
||||
@@ -2767,6 +2867,7 @@ describeEmbeddedPostgres("issueService blockers and dependency wake readiness",
|
||||
const projectId = randomUUID();
|
||||
const projectWorkspaceId = randomUUID();
|
||||
const executionWorkspaceId = randomUUID();
|
||||
const blockerRunId = randomUUID();
|
||||
|
||||
await db.insert(companies).values({
|
||||
id: companyId,
|
||||
@@ -2834,6 +2935,15 @@ describeEmbeddedPostgres("issueService blockers and dependency wake readiness",
|
||||
assigneeAgentId,
|
||||
},
|
||||
]);
|
||||
await db.insert(heartbeatRuns).values({
|
||||
id: blockerRunId,
|
||||
companyId,
|
||||
agentId: assigneeAgentId,
|
||||
invocationSource: "assignment",
|
||||
triggerDetail: "system",
|
||||
status: "succeeded",
|
||||
contextSnapshot: { issueId: blockerId, taskId: blockerId },
|
||||
});
|
||||
await svc.update(dependentId, { blockedByIssueIds: [blockerId] });
|
||||
|
||||
// A run touched the workspace (prepare phase) but has not yet recorded
|
||||
@@ -2841,6 +2951,7 @@ describeEmbeddedPostgres("issueService blockers and dependency wake readiness",
|
||||
await db.insert(workspaceOperations).values({
|
||||
companyId,
|
||||
executionWorkspaceId,
|
||||
heartbeatRunId: blockerRunId,
|
||||
phase: "worktree_prepare",
|
||||
status: "succeeded",
|
||||
startedAt: new Date("2026-05-23T22:00:00.000Z"),
|
||||
@@ -2857,6 +2968,7 @@ describeEmbeddedPostgres("issueService blockers and dependency wake readiness",
|
||||
await db.insert(workspaceOperations).values({
|
||||
companyId,
|
||||
executionWorkspaceId,
|
||||
heartbeatRunId: blockerRunId,
|
||||
phase: "workspace_finalize",
|
||||
status: "failed",
|
||||
startedAt: new Date("2026-05-23T22:05:00.000Z"),
|
||||
@@ -2868,6 +2980,7 @@ describeEmbeddedPostgres("issueService blockers and dependency wake readiness",
|
||||
await db.insert(workspaceOperations).values({
|
||||
companyId,
|
||||
executionWorkspaceId,
|
||||
heartbeatRunId: blockerRunId,
|
||||
phase: "workspace_finalize",
|
||||
status: "succeeded",
|
||||
startedAt: new Date("2026-05-23T22:10:00.000Z"),
|
||||
@@ -2886,6 +2999,154 @@ describeEmbeddedPostgres("issueService blockers and dependency wake readiness",
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps a reused-workspace blocker ready when a later sibling operation is unfinalized", async () => {
|
||||
const companyId = randomUUID();
|
||||
const assigneeAgentId = randomUUID();
|
||||
const projectId = randomUUID();
|
||||
const projectWorkspaceId = randomUUID();
|
||||
const executionWorkspaceId = randomUUID();
|
||||
const blockerRunId = randomUUID();
|
||||
const siblingRunId = randomUUID();
|
||||
|
||||
await db.insert(companies).values({
|
||||
id: companyId,
|
||||
name: "Paperclip",
|
||||
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
});
|
||||
await db.insert(agents).values({
|
||||
id: assigneeAgentId,
|
||||
companyId,
|
||||
name: "QA",
|
||||
role: "qa",
|
||||
status: "active",
|
||||
adapterType: "claude_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {},
|
||||
permissions: {},
|
||||
});
|
||||
await db.insert(projects).values({
|
||||
id: projectId,
|
||||
companyId,
|
||||
name: "Shared workspace project",
|
||||
status: "in_progress",
|
||||
});
|
||||
await db.insert(projectWorkspaces).values({
|
||||
id: projectWorkspaceId,
|
||||
companyId,
|
||||
projectId,
|
||||
name: "Shared workspace",
|
||||
sourceType: "local_path",
|
||||
visibility: "default",
|
||||
isPrimary: true,
|
||||
});
|
||||
await db.insert(executionWorkspaces).values({
|
||||
id: executionWorkspaceId,
|
||||
companyId,
|
||||
projectId,
|
||||
projectWorkspaceId,
|
||||
mode: "isolated_workspace",
|
||||
strategyType: "git_worktree",
|
||||
name: "Shared exec workspace",
|
||||
status: "active",
|
||||
providerType: "git_worktree",
|
||||
});
|
||||
|
||||
const blockerId = randomUUID();
|
||||
const siblingId = randomUUID();
|
||||
const dependentId = randomUUID();
|
||||
await db.insert(issues).values([
|
||||
{
|
||||
id: blockerId,
|
||||
companyId,
|
||||
projectId,
|
||||
title: "Completed blocker",
|
||||
status: "done",
|
||||
priority: "medium",
|
||||
executionWorkspaceId,
|
||||
},
|
||||
{
|
||||
id: siblingId,
|
||||
companyId,
|
||||
projectId,
|
||||
title: "Later sibling",
|
||||
status: "in_progress",
|
||||
priority: "medium",
|
||||
executionWorkspaceId,
|
||||
assigneeAgentId,
|
||||
},
|
||||
{
|
||||
id: dependentId,
|
||||
companyId,
|
||||
projectId,
|
||||
title: "Dependent",
|
||||
status: "blocked",
|
||||
priority: "medium",
|
||||
assigneeAgentId,
|
||||
},
|
||||
]);
|
||||
await db.insert(heartbeatRuns).values([
|
||||
{
|
||||
id: blockerRunId,
|
||||
companyId,
|
||||
agentId: assigneeAgentId,
|
||||
invocationSource: "assignment",
|
||||
triggerDetail: "system",
|
||||
status: "succeeded",
|
||||
contextSnapshot: { issueId: blockerId, taskId: blockerId },
|
||||
},
|
||||
{
|
||||
id: siblingRunId,
|
||||
companyId,
|
||||
agentId: assigneeAgentId,
|
||||
invocationSource: "assignment",
|
||||
triggerDetail: "system",
|
||||
status: "running",
|
||||
contextSnapshot: { issueId: siblingId, taskId: siblingId },
|
||||
},
|
||||
]);
|
||||
await db.insert(workspaceOperations).values([
|
||||
{
|
||||
companyId,
|
||||
executionWorkspaceId,
|
||||
heartbeatRunId: blockerRunId,
|
||||
phase: "worktree_prepare",
|
||||
status: "succeeded",
|
||||
startedAt: new Date("2026-05-23T22:00:00.000Z"),
|
||||
},
|
||||
{
|
||||
companyId,
|
||||
executionWorkspaceId,
|
||||
heartbeatRunId: blockerRunId,
|
||||
phase: "workspace_finalize",
|
||||
status: "succeeded",
|
||||
startedAt: new Date("2026-05-23T22:10:00.000Z"),
|
||||
},
|
||||
{
|
||||
companyId,
|
||||
executionWorkspaceId,
|
||||
heartbeatRunId: siblingRunId,
|
||||
phase: "worktree_prepare",
|
||||
status: "succeeded",
|
||||
startedAt: new Date("2026-05-23T22:15:00.000Z"),
|
||||
},
|
||||
]);
|
||||
await svc.update(dependentId, { blockedByIssueIds: [blockerId] });
|
||||
|
||||
await expect(svc.listWakeableBlockedDependents(blockerId)).resolves.toEqual([
|
||||
expect.objectContaining({
|
||||
id: dependentId,
|
||||
assigneeAgentId,
|
||||
blockerIssueIds: [blockerId],
|
||||
}),
|
||||
]);
|
||||
await expect(svc.getDependencyReadiness(dependentId)).resolves.toMatchObject({
|
||||
isDependencyReady: true,
|
||||
pendingFinalizeBlockerIssueIds: [],
|
||||
unresolvedBlockerIssueIds: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("treats blockers with no executionWorkspaceId as not subject to the workspace-finalize barrier", async () => {
|
||||
const companyId = randomUUID();
|
||||
const assigneeAgentId = randomUUID();
|
||||
@@ -3121,11 +3382,11 @@ describeEmbeddedPostgres("issueService blockers and dependency wake readiness",
|
||||
expect(await svc.getWakeableParentAfterChildCompletion(parentId)).toMatchObject({
|
||||
id: parentId,
|
||||
assigneeAgentId,
|
||||
childIssueIds: [childA, childB],
|
||||
childIssueSummaries: [
|
||||
childIssueIds: expect.arrayContaining([childA, childB]),
|
||||
childIssueSummaries: expect.arrayContaining([
|
||||
expect.objectContaining({ id: childA, title: "Child A", status: "done" }),
|
||||
expect.objectContaining({ id: childB, title: "Child B", status: "cancelled" }),
|
||||
],
|
||||
]),
|
||||
childIssueSummaryTruncated: false,
|
||||
});
|
||||
});
|
||||
@@ -3475,6 +3736,79 @@ describeEmbeddedPostgres("issueService.create workspace inheritance", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects cross-company workspace inheritance sources", async () => {
|
||||
const companyA = randomUUID();
|
||||
const companyB = randomUUID();
|
||||
const projectId = randomUUID();
|
||||
const sourceIssueId = randomUUID();
|
||||
const projectWorkspaceId = randomUUID();
|
||||
const executionWorkspaceId = randomUUID();
|
||||
|
||||
await db.insert(companies).values([
|
||||
{
|
||||
id: companyA,
|
||||
name: "Paperclip A",
|
||||
issuePrefix: `A${companyA.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
},
|
||||
{
|
||||
id: companyB,
|
||||
name: "Paperclip B",
|
||||
issuePrefix: `B${companyB.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
},
|
||||
]);
|
||||
await instanceSettingsService(db).updateExperimental({ enableIsolatedWorkspaces: true });
|
||||
|
||||
await db.insert(projects).values({
|
||||
id: projectId,
|
||||
companyId: companyB,
|
||||
name: "Foreign workspace project",
|
||||
status: "in_progress",
|
||||
});
|
||||
|
||||
await db.insert(projectWorkspaces).values({
|
||||
id: projectWorkspaceId,
|
||||
companyId: companyB,
|
||||
projectId,
|
||||
name: "Foreign workspace",
|
||||
});
|
||||
|
||||
await db.insert(executionWorkspaces).values({
|
||||
id: executionWorkspaceId,
|
||||
companyId: companyB,
|
||||
projectId,
|
||||
projectWorkspaceId,
|
||||
mode: "operator_branch",
|
||||
strategyType: "git_worktree",
|
||||
name: "Foreign operator branch",
|
||||
status: "active",
|
||||
providerType: "git_worktree",
|
||||
});
|
||||
|
||||
await db.insert(issues).values({
|
||||
id: sourceIssueId,
|
||||
companyId: companyB,
|
||||
projectId,
|
||||
projectWorkspaceId,
|
||||
title: "Foreign source issue",
|
||||
status: "todo",
|
||||
priority: "medium",
|
||||
executionWorkspaceId,
|
||||
executionWorkspacePreference: "reuse_existing",
|
||||
executionWorkspaceSettings: {
|
||||
mode: "operator_branch",
|
||||
},
|
||||
});
|
||||
|
||||
await expect(svc.create(companyA, {
|
||||
title: "Cross-company inheritance should fail",
|
||||
inheritExecutionWorkspaceFromIssueId: sourceIssueId,
|
||||
})).rejects.toMatchObject({
|
||||
status: 404,
|
||||
});
|
||||
});
|
||||
|
||||
it("derives project identity when an update adds workspace linkage to a projectless issue", async () => {
|
||||
const companyId = randomUUID();
|
||||
const projectId = randomUUID();
|
||||
|
||||
@@ -10407,12 +10407,19 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
|
||||
return { kind: "skipped" as const, dependencyGate: true as const };
|
||||
}
|
||||
|
||||
if (!activeExecutionRun && reason === BLOCKER_RELEASE_WAKE_REASON && effectiveIdempotencyKey) {
|
||||
if (reason === BLOCKER_RELEASE_WAKE_REASON && effectiveIdempotencyKey) {
|
||||
const existingReleaseWake = await findExistingBlockerReleaseWake(tx, {
|
||||
companyId: issue.companyId,
|
||||
idempotencyKey: effectiveIdempotencyKey,
|
||||
});
|
||||
if (existingReleaseWake) {
|
||||
const existingReleaseRun = existingReleaseWake.runId
|
||||
? await tx
|
||||
.select()
|
||||
.from(heartbeatRuns)
|
||||
.where(eq(heartbeatRuns.id, existingReleaseWake.runId))
|
||||
.then((rows) => rows[0] ?? null)
|
||||
: null;
|
||||
await tx.insert(agentWakeupRequests).values({
|
||||
companyId: agent.companyId,
|
||||
agentId,
|
||||
@@ -10435,7 +10442,9 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(agentWakeupRequests.id, existingReleaseWake.id));
|
||||
return { kind: "duplicate" as const };
|
||||
return existingReleaseRun
|
||||
? { kind: "existing" as const, run: existingReleaseRun }
|
||||
: { kind: "duplicate" as const };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10601,6 +10610,7 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
|
||||
});
|
||||
|
||||
if (outcome.kind === "deferred" || outcome.kind === "duplicate") return null;
|
||||
if (outcome.kind === "existing") return outcome.run;
|
||||
if (outcome.kind === "skipped") {
|
||||
if ("dependencyGate" in outcome && outcome.dependencyGate && reason === BLOCKER_RELEASE_WAKE_REASON) {
|
||||
await requeueBlockerReleaseWakeIfDependencyGateCleared({
|
||||
|
||||
@@ -655,6 +655,77 @@ export async function listUnfinalizedExecutionWorkspaceIds(
|
||||
return unfinalized;
|
||||
}
|
||||
|
||||
type DoneBlockerFinalizeTarget = {
|
||||
blockerIssueId: string;
|
||||
executionWorkspaceId: string | null;
|
||||
};
|
||||
|
||||
async function listPendingFinalizeBlockerIssueIds(
|
||||
dbOrTx: Pick<Db, "select">,
|
||||
companyId: string,
|
||||
blockers: DoneBlockerFinalizeTarget[],
|
||||
): Promise<Set<string>> {
|
||||
const targets = blockers.filter((blocker) => blocker.executionWorkspaceId);
|
||||
const pending = new Set<string>();
|
||||
if (targets.length === 0) return pending;
|
||||
|
||||
const blockerIdsByWorkspaceId = new Map<string, Set<string>>();
|
||||
for (const target of targets) {
|
||||
if (!target.executionWorkspaceId) continue;
|
||||
const blockerIds = blockerIdsByWorkspaceId.get(target.executionWorkspaceId) ?? new Set<string>();
|
||||
blockerIds.add(target.blockerIssueId);
|
||||
blockerIdsByWorkspaceId.set(target.executionWorkspaceId, blockerIds);
|
||||
}
|
||||
|
||||
const rows = await dbOrTx
|
||||
.select({
|
||||
executionWorkspaceId: workspaceOperations.executionWorkspaceId,
|
||||
phase: workspaceOperations.phase,
|
||||
status: workspaceOperations.status,
|
||||
startedAt: workspaceOperations.startedAt,
|
||||
contextIssueId: sql<string | null>`${heartbeatRuns.contextSnapshot} ->> 'issueId'`,
|
||||
contextTaskId: sql<string | null>`${heartbeatRuns.contextSnapshot} ->> 'taskId'`,
|
||||
})
|
||||
.from(workspaceOperations)
|
||||
.leftJoin(heartbeatRuns, eq(workspaceOperations.heartbeatRunId, heartbeatRuns.id))
|
||||
.where(
|
||||
and(
|
||||
eq(workspaceOperations.companyId, companyId),
|
||||
inArray(workspaceOperations.executionWorkspaceId, [...blockerIdsByWorkspaceId.keys()]),
|
||||
),
|
||||
);
|
||||
|
||||
const latestByBlockerIssueId = new Map<string, { phase: string; status: string; startedAt: Date }>();
|
||||
for (const row of rows) {
|
||||
if (!row.executionWorkspaceId) continue;
|
||||
const blockerIds = blockerIdsByWorkspaceId.get(row.executionWorkspaceId);
|
||||
if (!blockerIds) continue;
|
||||
const runIssueIds = [row.contextIssueId, row.contextTaskId].filter(
|
||||
(value): value is string => typeof value === "string" && value.length > 0,
|
||||
);
|
||||
for (const blockerIssueId of runIssueIds) {
|
||||
if (!blockerIds.has(blockerIssueId)) continue;
|
||||
const current = latestByBlockerIssueId.get(blockerIssueId);
|
||||
if (!current || row.startedAt > current.startedAt) {
|
||||
latestByBlockerIssueId.set(blockerIssueId, {
|
||||
phase: row.phase,
|
||||
status: row.status,
|
||||
startedAt: row.startedAt,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const target of targets) {
|
||||
const latest = latestByBlockerIssueId.get(target.blockerIssueId);
|
||||
if (!latest) continue; // no blocker-specific workspace ops recorded -> no finalize barrier
|
||||
if (latest.phase === "workspace_finalize" && latest.status === "succeeded") continue;
|
||||
pending.add(target.blockerIssueId);
|
||||
}
|
||||
|
||||
return pending;
|
||||
}
|
||||
|
||||
async function listIssueDependencyReadinessMap(
|
||||
dbOrTx: Pick<Db, "select">,
|
||||
companyId: string,
|
||||
@@ -680,23 +751,28 @@ async function listIssueDependencyReadinessMap(
|
||||
and(
|
||||
eq(issueRelations.companyId, companyId),
|
||||
eq(issueRelations.type, "blocks"),
|
||||
eq(issues.companyId, companyId),
|
||||
inArray(issueRelations.relatedIssueId, uniqueIssueIds),
|
||||
),
|
||||
);
|
||||
|
||||
// Collect executionWorkspaceIds of "done" blockers — these are the only ones
|
||||
// subject to the workspace-finalize barrier. Blockers that aren't done already
|
||||
// mark the dependent as not-ready and don't need a finalize check.
|
||||
const doneBlockerWorkspaceIds = new Set<string>();
|
||||
// Collect "done" blockers with execution workspaces — these are the only ones
|
||||
// subject to the workspace-finalize barrier. The barrier is blocker-specific:
|
||||
// a later sibling can reuse the same workspace without making this already-done
|
||||
// blocker unresolved again.
|
||||
const doneBlockers: DoneBlockerFinalizeTarget[] = [];
|
||||
for (const row of blockerRows) {
|
||||
if (row.blockerStatus === "done" && row.blockerExecutionWorkspaceId) {
|
||||
doneBlockerWorkspaceIds.add(row.blockerExecutionWorkspaceId);
|
||||
doneBlockers.push({
|
||||
blockerIssueId: row.blockerIssueId,
|
||||
executionWorkspaceId: row.blockerExecutionWorkspaceId,
|
||||
});
|
||||
}
|
||||
}
|
||||
const unfinalizedWorkspaceIds = await listUnfinalizedExecutionWorkspaceIds(
|
||||
const pendingFinalizeBlockerIssueIds = await listPendingFinalizeBlockerIssueIds(
|
||||
dbOrTx,
|
||||
companyId,
|
||||
[...doneBlockerWorkspaceIds],
|
||||
doneBlockers,
|
||||
);
|
||||
|
||||
for (const row of blockerRows) {
|
||||
@@ -711,9 +787,9 @@ async function listIssueDependencyReadinessMap(
|
||||
current.isDependencyReady = false;
|
||||
} else if (
|
||||
row.blockerExecutionWorkspaceId &&
|
||||
unfinalizedWorkspaceIds.has(row.blockerExecutionWorkspaceId)
|
||||
pendingFinalizeBlockerIssueIds.has(row.blockerIssueId)
|
||||
) {
|
||||
// Workspace-finalize barrier: the blocker's most recent run on its
|
||||
// Workspace-finalize barrier: the blocker's own most recent run on its
|
||||
// execution workspace hasn't recorded a successful workspace_finalize.
|
||||
// Treat the dependent as not-ready until sync-back lands (or the run
|
||||
// finalizes); a subsequent finalize wake will re-evaluate readiness.
|
||||
@@ -2145,6 +2221,7 @@ async function blockedByMapForIssues(
|
||||
and(
|
||||
eq(issueRelations.companyId, companyId),
|
||||
eq(issueRelations.type, "blocks"),
|
||||
eq(issues.companyId, companyId),
|
||||
inArray(issueRelations.relatedIssueId, issueIdChunk),
|
||||
),
|
||||
);
|
||||
@@ -3525,6 +3602,7 @@ export function issueService(db: Db) {
|
||||
and(
|
||||
eq(issueRelations.companyId, companyId),
|
||||
eq(issueRelations.type, "blocks"),
|
||||
eq(issues.companyId, companyId),
|
||||
inArray(issueRelations.relatedIssueId, uniqueIssueIds),
|
||||
),
|
||||
),
|
||||
@@ -3545,6 +3623,7 @@ export function issueService(db: Db) {
|
||||
and(
|
||||
eq(issueRelations.companyId, companyId),
|
||||
eq(issueRelations.type, "blocks"),
|
||||
eq(issues.companyId, companyId),
|
||||
inArray(issueRelations.issueId, uniqueIssueIds),
|
||||
),
|
||||
),
|
||||
@@ -4256,6 +4335,7 @@ export function issueService(db: Db) {
|
||||
eq(issueRelations.companyId, blockerIssue.companyId),
|
||||
eq(issueRelations.type, "blocks"),
|
||||
eq(issueRelations.issueId, blockerIssueId),
|
||||
eq(issues.companyId, blockerIssue.companyId),
|
||||
),
|
||||
);
|
||||
if (candidates.length === 0) return [];
|
||||
|
||||
Reference in new issue
Block a user