Merge pull request 'fix(routing): comment→CEO wakeup reads issue id from event.entityId' (#2) from fix/comment-routing-ceo-entityid into main

This commit was merged in pull request #2.
This commit is contained in:
2026-06-17 04:55:46 +00:00

View File

@@ -540,27 +540,36 @@ const plugin = definePlugin({
if (event.actorType !== "user") return; if (event.actorType !== "user") return;
if (!event.companyId) return; if (!event.companyId) return;
// entityId is the comment ID — fetch the comment to get issueId + body // Event/payload shape for `issue.comment.created` (Paperclip host):
const entityId = event.entityId; // event.entityId → the ISSUE id (the primary entity of the event)
if (!entityId) return; // event.payload.commentId → the comment id
// event.payload.bodySnippet → TRUNCATED body (not the full text)
// The event payload may contain the issueId directly // event.payload.reopened / reopenedFrom → set when the comment
const payload = event.payload as { // reopened a `done` issue (host then natively wakes its assignee)
// NOTE: the host does NOT send `payload.issueId` and does NOT send the
// full `body`. The earlier code read `payload.issueId` (always absent)
// and skipped every event — silently disabling comment→CEO routing.
// See @paperclipai/plugin-sdk index.d.ts: `issueId: event.entityId`.
const payload = (event.payload ?? null) as {
issueId?: string; issueId?: string;
commentId?: string;
body?: string; body?: string;
bodySnippet?: string;
reopened?: boolean;
reopenedFrom?: string;
} | null; } | null;
const issueId = payload?.issueId; // issueId: prefer entityId (current host), fall back to payload for
let commentBody = payload?.body; // forward/backward compatibility with other host versions.
const issueId = event.entityId || payload?.issueId;
// If issueId is not in payload, try to find it from the comment entity
if (!issueId) { if (!issueId) {
ctx.logger.warn( ctx.logger.warn(
"issue.comment.created event missing issueId in payload, skipping", "issue.comment.created event missing issueId (entityId+payload empty), skipping",
{ entityId, payload }, { entityId: event.entityId, payload },
); );
return; return;
} }
const commentId = payload?.commentId;
// Fetch issue details for context // Fetch issue details for context
const issue = await ctx.issues.get(issueId, event.companyId); const issue = await ctx.issues.get(issueId, event.companyId);
@@ -571,20 +580,6 @@ const plugin = definePlugin({
return; return;
} }
// If comment body not in payload, fetch from API
if (!commentBody) {
try {
const comments = await ctx.issues.listComments(
issueId,
event.companyId,
);
const latest = comments[comments.length - 1];
commentBody = latest?.body || "(לא ניתן לקרוא את התגובה)";
} catch {
commentBody = "(שגיאה בקריאת התגובה)";
}
}
// Wake the CEO agent for this company // Wake the CEO agent for this company
const ceoAgentId = CEO_AGENT_IDS[event.companyId]; const ceoAgentId = CEO_AGENT_IDS[event.companyId];
if (!ceoAgentId) { if (!ceoAgentId) {
@@ -594,6 +589,45 @@ const plugin = definePlugin({
return; return;
} }
// Dedup against the host's native reopen-on-comment wake: when a comment
// reopens a `done` issue, the host already wakes that issue's assignee.
// If the assignee IS this company's CEO, that native wake covers us —
// invoking again would double-run the CEO. Skip ONLY in that exact case.
// For issues owned by any other agent (e.g. an analyst sub-task), the
// native wake targets the wrong agent (and the queued run is cancelled
// with `issue_assignee_changed`), so we MUST route the comment to the CEO.
if (issue.assigneeAgentId === ceoAgentId && payload?.reopened === true) {
ctx.logger.info(
"Comment reopened CEO-owned issue; native wake handles it, skipping plugin route",
{ issueId, ceoAgentId },
);
return;
}
// Resolve the full comment body. The payload only carries a truncated
// snippet, so fetch the comment list and match by id (fall back to the
// latest comment, then the snippet).
let commentBody = payload?.body;
if (!commentBody) {
try {
const comments = await ctx.issues.listComments(
issueId,
event.companyId,
);
const matched = commentId
? comments.find((c) => c.id === commentId)
: undefined;
const latest = comments[comments.length - 1];
commentBody =
matched?.body ||
latest?.body ||
payload?.bodySnippet ||
"(לא ניתן לקרוא את התגובה)";
} catch {
commentBody = payload?.bodySnippet || "(שגיאה בקריאת התגובה)";
}
}
try { try {
const { runId } = await ctx.agents.invoke(ceoAgentId, event.companyId, { const { runId } = await ctx.agents.invoke(ceoAgentId, event.companyId, {
prompt: [ prompt: [
@@ -608,7 +642,7 @@ const plugin = definePlugin({
}); });
ctx.logger.info("Routed user comment to CEO agent", { ctx.logger.info("Routed user comment to CEO agent", {
issueId, issueId,
commentId: entityId, commentId: commentId || null,
runId, runId,
}); });
} catch (err) { } catch (err) {