Add issue ancestors, @-mention wakeups on comments
- issueService.getAncestors() walks parent chain, returning up to 50 ancestors - GET /issues/:id now includes ancestors array for context delivery to agents - POST /issues/:id/comments now parses @-mentions and wakes mentioned agents Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -35,7 +35,8 @@ export function issueRoutes(db: Db) {
|
||||
return;
|
||||
}
|
||||
assertCompanyAccess(req, issue.companyId);
|
||||
res.json(issue);
|
||||
const ancestors = await svc.getAncestors(id);
|
||||
res.json({ ...issue, ancestors });
|
||||
});
|
||||
|
||||
router.post("/companies/:companyId/issues", validate(createIssueSchema), async (req, res) => {
|
||||
@@ -262,6 +263,21 @@ export function issueRoutes(db: Db) {
|
||||
details: { commentId: comment.id },
|
||||
});
|
||||
|
||||
// @-mention wakeups
|
||||
svc.findMentionedAgents(issue.companyId, req.body.body).then((ids) => {
|
||||
for (const mentionedId of ids) {
|
||||
heartbeat.wakeup(mentionedId, {
|
||||
source: "automation",
|
||||
triggerDetail: "system",
|
||||
reason: `Mentioned in comment on issue ${id}`,
|
||||
payload: { issueId: id, commentId: comment.id },
|
||||
requestedByActorType: actor.actorType,
|
||||
requestedByActorId: actor.actorId,
|
||||
contextSnapshot: { issueId: id, commentId: comment.id, source: "comment.mention" },
|
||||
}).catch((err) => logger.warn({ err, agentId: mentionedId }, "failed to wake mentioned agent"));
|
||||
}
|
||||
}).catch((err) => logger.warn({ err, issueId: id }, "failed to resolve @-mentions"));
|
||||
|
||||
res.status(201).json(comment);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { and, desc, eq, inArray, isNull, or, sql } from "drizzle-orm";
|
||||
import type { Db } from "@paperclip/db";
|
||||
import { issues, issueComments } from "@paperclip/db";
|
||||
import { agents, issues, issueComments } from "@paperclip/db";
|
||||
import { conflict, notFound, unprocessable } from "../errors.js";
|
||||
|
||||
const ISSUE_TRANSITIONS: Record<string, string[]> = {
|
||||
@@ -216,6 +216,46 @@ export function issueService(db: Db) {
|
||||
.then((rows) => rows[0]);
|
||||
},
|
||||
|
||||
findMentionedAgents: async (companyId: string, body: string) => {
|
||||
const re = /\B@([^\s@,!?.]+)/g;
|
||||
const tokens = new Set<string>();
|
||||
let m: RegExpExecArray | null;
|
||||
while ((m = re.exec(body)) !== null) tokens.add(m[1].toLowerCase());
|
||||
if (tokens.size === 0) return [];
|
||||
const rows = await db.select({ id: agents.id, name: agents.name })
|
||||
.from(agents).where(eq(agents.companyId, companyId));
|
||||
return rows.filter(a => tokens.has(a.name.toLowerCase())).map(a => a.id);
|
||||
},
|
||||
|
||||
getAncestors: async (issueId: string) => {
|
||||
const ancestors: Array<{
|
||||
id: string; title: string; description: string | null;
|
||||
status: string; priority: string;
|
||||
assigneeAgentId: string | null; projectId: string | null; goalId: string | null;
|
||||
}> = [];
|
||||
const visited = new Set<string>([issueId]);
|
||||
const start = await db.select().from(issues).where(eq(issues.id, issueId)).then(r => r[0] ?? null);
|
||||
let currentId = start?.parentId ?? null;
|
||||
while (currentId && !visited.has(currentId) && ancestors.length < 50) {
|
||||
visited.add(currentId);
|
||||
const parent = await db.select({
|
||||
id: issues.id, title: issues.title, description: issues.description,
|
||||
status: issues.status, priority: issues.priority,
|
||||
assigneeAgentId: issues.assigneeAgentId, projectId: issues.projectId,
|
||||
goalId: issues.goalId, parentId: issues.parentId,
|
||||
}).from(issues).where(eq(issues.id, currentId)).then(r => r[0] ?? null);
|
||||
if (!parent) break;
|
||||
ancestors.push({
|
||||
id: parent.id, title: parent.title, description: parent.description ?? null,
|
||||
status: parent.status, priority: parent.priority,
|
||||
assigneeAgentId: parent.assigneeAgentId ?? null,
|
||||
projectId: parent.projectId ?? null, goalId: parent.goalId ?? null,
|
||||
});
|
||||
currentId = parent.parentId ?? null;
|
||||
}
|
||||
return ancestors;
|
||||
},
|
||||
|
||||
staleCount: async (companyId: string, minutes = 60) => {
|
||||
const cutoff = new Date(Date.now() - minutes * 60 * 1000);
|
||||
const result = await db
|
||||
|
||||
Reference in New Issue
Block a user