diff --git a/src/worker.ts b/src/worker.ts index 57468fb..f02dd9a 100644 --- a/src/worker.ts +++ b/src/worker.ts @@ -540,27 +540,36 @@ const plugin = definePlugin({ if (event.actorType !== "user") return; if (!event.companyId) return; - // entityId is the comment ID — fetch the comment to get issueId + body - const entityId = event.entityId; - if (!entityId) return; - - // The event payload may contain the issueId directly - const payload = event.payload as { + // Event/payload shape for `issue.comment.created` (Paperclip host): + // event.entityId → the ISSUE id (the primary entity of the event) + // event.payload.commentId → the comment id + // event.payload.bodySnippet → TRUNCATED body (not the full text) + // event.payload.reopened / reopenedFrom → set when the comment + // 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; + commentId?: string; body?: string; + bodySnippet?: string; + reopened?: boolean; + reopenedFrom?: string; } | null; - const issueId = payload?.issueId; - let commentBody = payload?.body; - - // If issueId is not in payload, try to find it from the comment entity + // issueId: prefer entityId (current host), fall back to payload for + // forward/backward compatibility with other host versions. + const issueId = event.entityId || payload?.issueId; if (!issueId) { ctx.logger.warn( - "issue.comment.created event missing issueId in payload, skipping", - { entityId, payload }, + "issue.comment.created event missing issueId (entityId+payload empty), skipping", + { entityId: event.entityId, payload }, ); return; } + const commentId = payload?.commentId; // Fetch issue details for context const issue = await ctx.issues.get(issueId, event.companyId); @@ -571,20 +580,6 @@ const plugin = definePlugin({ 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 const ceoAgentId = CEO_AGENT_IDS[event.companyId]; if (!ceoAgentId) { @@ -594,6 +589,45 @@ const plugin = definePlugin({ 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 { const { runId } = await ctx.agents.invoke(ceoAgentId, event.companyId, { prompt: [ @@ -608,7 +642,7 @@ const plugin = definePlugin({ }); ctx.logger.info("Routed user comment to CEO agent", { issueId, - commentId: entityId, + commentId: commentId || null, runId, }); } catch (err) {