mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-15 10:30:37 +09:00
Add blocker relations and dependency wakeups
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
2f73346a64
commit
dde4cc070e
18 changed files with 13924 additions and 69 deletions
|
|
@ -442,11 +442,12 @@ export function issueRoutes(
|
|||
return;
|
||||
}
|
||||
assertCompanyAccess(req, issue.companyId);
|
||||
const [{ project, goal }, ancestors, mentionedProjectIds, documentPayload] = await Promise.all([
|
||||
const [{ project, goal }, ancestors, mentionedProjectIds, documentPayload, relations] = await Promise.all([
|
||||
resolveIssueProjectAndGoal(issue),
|
||||
svc.getAncestors(issue.id),
|
||||
svc.findMentionedProjectIds(issue.id),
|
||||
documentsSvc.getIssueDocumentPayload(issue),
|
||||
svc.getRelationSummaries(issue.id),
|
||||
]);
|
||||
const mentionedProjects = mentionedProjectIds.length > 0
|
||||
? await projectsSvc.listByIds(issue.companyId, mentionedProjectIds)
|
||||
|
|
@ -459,6 +460,8 @@ export function issueRoutes(
|
|||
...issue,
|
||||
goalId: goal?.id ?? issue.goalId,
|
||||
ancestors,
|
||||
blockedBy: relations.blockedBy,
|
||||
blocks: relations.blocks,
|
||||
...documentPayload,
|
||||
project: project ?? null,
|
||||
goal: goal ?? null,
|
||||
|
|
@ -482,11 +485,13 @@ export function issueRoutes(
|
|||
? req.query.wakeCommentId.trim()
|
||||
: null;
|
||||
|
||||
const [{ project, goal }, ancestors, commentCursor, wakeComment, attachments] = await Promise.all([
|
||||
const [{ project, goal }, ancestors, commentCursor, wakeComment, relations, attachments] =
|
||||
await Promise.all([
|
||||
resolveIssueProjectAndGoal(issue),
|
||||
svc.getAncestors(issue.id),
|
||||
svc.getCommentCursor(issue.id),
|
||||
wakeCommentId ? svc.getComment(wakeCommentId) : null,
|
||||
svc.getRelationSummaries(issue.id),
|
||||
svc.listAttachments(issue.id),
|
||||
]);
|
||||
|
||||
|
|
@ -501,6 +506,8 @@ export function issueRoutes(
|
|||
projectId: issue.projectId,
|
||||
goalId: goal?.id ?? issue.goalId,
|
||||
parentId: issue.parentId,
|
||||
blockedBy: relations.blockedBy,
|
||||
blocks: relations.blocks,
|
||||
assigneeAgentId: issue.assigneeAgentId,
|
||||
assigneeUserId: issue.assigneeUserId,
|
||||
updatedAt: issue.updatedAt,
|
||||
|
|
@ -1058,7 +1065,11 @@ export function issueRoutes(
|
|||
action: "issue.created",
|
||||
entityType: "issue",
|
||||
entityId: issue.id,
|
||||
details: { title: issue.title, identifier: issue.identifier },
|
||||
details: {
|
||||
title: issue.title,
|
||||
identifier: issue.identifier,
|
||||
...(Array.isArray(req.body.blockedByIssueIds) ? { blockedByIssueIds: req.body.blockedByIssueIds } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
void queueIssueAssignmentWakeup({
|
||||
|
|
@ -1104,6 +1115,10 @@ export function issueRoutes(
|
|||
|
||||
const actor = getActorInfo(req);
|
||||
const isClosed = existing.status === "done" || existing.status === "cancelled";
|
||||
const existingRelations =
|
||||
Array.isArray(req.body.blockedByIssueIds)
|
||||
? await svc.getRelationSummaries(existing.id)
|
||||
: null;
|
||||
const {
|
||||
comment: commentBody,
|
||||
reopen: reopenRequested,
|
||||
|
|
@ -1158,7 +1173,11 @@ export function issueRoutes(
|
|||
}
|
||||
let issue;
|
||||
try {
|
||||
issue = await svc.update(id, updateFields);
|
||||
issue = await svc.update(id, {
|
||||
...updateFields,
|
||||
actorAgentId: actor.agentId ?? null,
|
||||
actorUserId: actor.actorType === "user" ? actor.actorId : null,
|
||||
});
|
||||
} catch (err) {
|
||||
if (err instanceof HttpError && err.status === 422) {
|
||||
logger.warn(
|
||||
|
|
@ -1187,6 +1206,15 @@ export function issueRoutes(
|
|||
res.status(404).json({ error: "Issue not found" });
|
||||
return;
|
||||
}
|
||||
let issueResponse: typeof issue & { blockedBy?: unknown; blocks?: unknown } = issue;
|
||||
if (issue && Array.isArray(req.body.blockedByIssueIds)) {
|
||||
const updatedRelations = await svc.getRelationSummaries(issue.id);
|
||||
issueResponse = {
|
||||
...issue,
|
||||
blockedBy: updatedRelations.blockedBy,
|
||||
blocks: updatedRelations.blocks,
|
||||
};
|
||||
}
|
||||
await routinesSvc.syncRunStatusForIssue(issue.id);
|
||||
|
||||
if (actor.runId) {
|
||||
|
|
@ -1201,6 +1229,9 @@ export function issueRoutes(
|
|||
previous[key] = (existing as Record<string, unknown>)[key];
|
||||
}
|
||||
}
|
||||
if (Array.isArray(req.body.blockedByIssueIds)) {
|
||||
previous.blockedByIssueIds = existingRelations?.blockedBy.map((relation) => relation.id) ?? [];
|
||||
}
|
||||
|
||||
const hasFieldChanges = Object.keys(previous).length > 0;
|
||||
const reopened =
|
||||
|
|
@ -1229,6 +1260,31 @@ export function issueRoutes(
|
|||
},
|
||||
});
|
||||
|
||||
if (Array.isArray(req.body.blockedByIssueIds)) {
|
||||
const previousBlockedByIds = new Set((existingRelations?.blockedBy ?? []).map((relation) => relation.id));
|
||||
const nextBlockedByIds = new Set(req.body.blockedByIssueIds as string[]);
|
||||
const addedBlockedByIssueIds = [...nextBlockedByIds].filter((candidate) => !previousBlockedByIds.has(candidate));
|
||||
const removedBlockedByIssueIds = [...previousBlockedByIds].filter((candidate) => !nextBlockedByIds.has(candidate));
|
||||
if (addedBlockedByIssueIds.length > 0 || removedBlockedByIssueIds.length > 0) {
|
||||
await logActivity(db, {
|
||||
companyId: issue.companyId,
|
||||
actorType: actor.actorType,
|
||||
actorId: actor.actorId,
|
||||
agentId: actor.agentId,
|
||||
runId: actor.runId,
|
||||
action: "issue.blockers_updated",
|
||||
entityType: "issue",
|
||||
entityId: issue.id,
|
||||
details: {
|
||||
identifier: issue.identifier,
|
||||
blockedByIssueIds: req.body.blockedByIssueIds,
|
||||
addedBlockedByIssueIds,
|
||||
removedBlockedByIssueIds,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (issue.status === "done" && existing.status !== "done") {
|
||||
const tc = getTelemetryClient();
|
||||
if (tc && actor.agentId) {
|
||||
|
|
@ -1277,10 +1333,18 @@ export function issueRoutes(
|
|||
|
||||
// Merge all wakeups from this update into one enqueue per agent to avoid duplicate runs.
|
||||
void (async () => {
|
||||
const wakeups = new Map<string, Parameters<typeof heartbeat.wakeup>[1]>();
|
||||
type WakeupRequest = NonNullable<Parameters<typeof heartbeat.wakeup>[1]>;
|
||||
const wakeups = new Map<string, { agentId: string; wakeup: WakeupRequest }>();
|
||||
const addWakeup = (agentId: string, wakeup: WakeupRequest) => {
|
||||
const wakeIssueId =
|
||||
wakeup.payload && typeof wakeup.payload === "object" && typeof wakeup.payload.issueId === "string"
|
||||
? wakeup.payload.issueId
|
||||
: issue.id;
|
||||
wakeups.set(`${agentId}:${wakeIssueId}`, { agentId, wakeup });
|
||||
};
|
||||
|
||||
if (assigneeChanged && issue.assigneeAgentId && issue.status !== "backlog") {
|
||||
wakeups.set(issue.assigneeAgentId, {
|
||||
addWakeup(issue.assigneeAgentId, {
|
||||
source: "assignment",
|
||||
triggerDetail: "system",
|
||||
reason: "issue_assigned",
|
||||
|
|
@ -1300,7 +1364,7 @@ export function issueRoutes(
|
|||
}
|
||||
|
||||
if (!assigneeChanged && statusChangedFromBacklog && issue.assigneeAgentId) {
|
||||
wakeups.set(issue.assigneeAgentId, {
|
||||
addWakeup(issue.assigneeAgentId, {
|
||||
source: "automation",
|
||||
triggerDetail: "system",
|
||||
reason: "issue_status_changed",
|
||||
|
|
@ -1328,9 +1392,8 @@ export function issueRoutes(
|
|||
}
|
||||
|
||||
for (const mentionedId of mentionedIds) {
|
||||
if (wakeups.has(mentionedId)) continue;
|
||||
if (actor.actorType === "agent" && actor.actorId === mentionedId) continue;
|
||||
wakeups.set(mentionedId, {
|
||||
addWakeup(mentionedId, {
|
||||
source: "automation",
|
||||
triggerDetail: "system",
|
||||
reason: "issue_comment_mentioned",
|
||||
|
|
@ -1349,14 +1412,69 @@ export function issueRoutes(
|
|||
}
|
||||
}
|
||||
|
||||
for (const [agentId, wakeup] of wakeups.entries()) {
|
||||
const becameDone = existing.status !== "done" && issue.status === "done";
|
||||
if (becameDone) {
|
||||
const dependents = await svc.listWakeableBlockedDependents(issue.id);
|
||||
for (const dependent of dependents) {
|
||||
addWakeup(dependent.assigneeAgentId, {
|
||||
source: "automation",
|
||||
triggerDetail: "system",
|
||||
reason: "issue_blockers_resolved",
|
||||
payload: {
|
||||
issueId: dependent.id,
|
||||
resolvedBlockerIssueId: issue.id,
|
||||
blockerIssueIds: dependent.blockerIssueIds,
|
||||
},
|
||||
requestedByActorType: actor.actorType,
|
||||
requestedByActorId: actor.actorId,
|
||||
contextSnapshot: {
|
||||
issueId: dependent.id,
|
||||
taskId: dependent.id,
|
||||
wakeReason: "issue_blockers_resolved",
|
||||
source: "issue.blockers_resolved",
|
||||
resolvedBlockerIssueId: issue.id,
|
||||
blockerIssueIds: dependent.blockerIssueIds,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const becameTerminal =
|
||||
!["done", "cancelled"].includes(existing.status) && ["done", "cancelled"].includes(issue.status);
|
||||
if (becameTerminal && issue.parentId) {
|
||||
const parent = await svc.getWakeableParentAfterChildCompletion(issue.parentId);
|
||||
if (parent) {
|
||||
addWakeup(parent.assigneeAgentId, {
|
||||
source: "automation",
|
||||
triggerDetail: "system",
|
||||
reason: "issue_children_completed",
|
||||
payload: {
|
||||
issueId: parent.id,
|
||||
completedChildIssueId: issue.id,
|
||||
childIssueIds: parent.childIssueIds,
|
||||
},
|
||||
requestedByActorType: actor.actorType,
|
||||
requestedByActorId: actor.actorId,
|
||||
contextSnapshot: {
|
||||
issueId: parent.id,
|
||||
taskId: parent.id,
|
||||
wakeReason: "issue_children_completed",
|
||||
source: "issue.children_completed",
|
||||
completedChildIssueId: issue.id,
|
||||
childIssueIds: parent.childIssueIds,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const { agentId, wakeup } of wakeups.values()) {
|
||||
heartbeat
|
||||
.wakeup(agentId, wakeup)
|
||||
.catch((err) => logger.warn({ err, issueId: issue.id, agentId }, "failed to wake agent on issue update"));
|
||||
}
|
||||
})();
|
||||
|
||||
res.json({ ...issue, comment });
|
||||
res.json({ ...issueResponse, comment });
|
||||
});
|
||||
|
||||
router.delete("/issues/:id", async (req, res) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue