feat(trigger): inherit thread-root mentions for reply-triggered agent tasks (#375)
When a top-level comment @mentions an agent (non-assignee), subsequent replies in the same thread now also trigger that agent via on_mention. Previously only the current comment's mentions were checked, so replies without an explicit re-mention would silently skip the agent. Extends enqueueMentionedAgentTasks to accept the parent comment and merge its parsed mentions (deduplicated) into the trigger set, reusing all existing guards (self-trigger, assignee skip, visibility, dedup). Closes MUL-177
This commit is contained in:
parent
b84543e634
commit
c9c8230271
2 changed files with 72 additions and 7 deletions
|
|
@ -323,6 +323,52 @@ func TestCommentTriggerOnMentionNoStatusGate(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestCommentTriggerThreadInheritedMention verifies that when a top-level
|
||||
// comment @mentions an agent (not the assignee), replies in that thread
|
||||
// also trigger the mentioned agent — even without explicitly re-mentioning it.
|
||||
func TestCommentTriggerThreadInheritedMention(t *testing.T) {
|
||||
agentID := getAgentID(t)
|
||||
|
||||
// Create an issue NOT assigned to the agent, so on_comment won't fire.
|
||||
issueID := createIssue(t, "Thread-inherited mention test")
|
||||
t.Cleanup(func() {
|
||||
clearTasks(t, issueID)
|
||||
resp := authRequest(t, "DELETE", "/api/issues/"+issueID, nil)
|
||||
resp.Body.Close()
|
||||
})
|
||||
|
||||
t.Run("reply in thread inherits parent mention", func(t *testing.T) {
|
||||
clearTasks(t, issueID)
|
||||
// Top-level comment @mentions the agent.
|
||||
content := fmt.Sprintf("[@Agent](mention://agent/%s) can you review this?", agentID)
|
||||
threadID := postComment(t, issueID, content, nil)
|
||||
if n := countPendingTasks(t, issueID); n != 1 {
|
||||
t.Fatalf("expected 1 pending task after initial mention, got %d", n)
|
||||
}
|
||||
// Clear the task so we can test the reply independently.
|
||||
clearTasks(t, issueID)
|
||||
// Reply in the thread WITHOUT mentioning the agent.
|
||||
postComment(t, issueID, "Here is more context for you", strPtr(threadID))
|
||||
if n := countPendingTasks(t, issueID); n != 1 {
|
||||
t.Errorf("expected 1 pending task from thread-inherited mention, got %d", n)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("reply does not double-trigger when re-mentioning same agent", func(t *testing.T) {
|
||||
clearTasks(t, issueID)
|
||||
// Top-level comment @mentions the agent.
|
||||
content := fmt.Sprintf("[@Agent](mention://agent/%s) help", agentID)
|
||||
threadID := postComment(t, issueID, content, nil)
|
||||
clearTasks(t, issueID)
|
||||
// Reply also @mentions the same agent — should still be just 1 task.
|
||||
reply := fmt.Sprintf("[@Agent](mention://agent/%s) any update?", agentID)
|
||||
postComment(t, issueID, reply, strPtr(threadID))
|
||||
if n := countPendingTasks(t, issueID); n != 1 {
|
||||
t.Errorf("expected 1 pending task (no duplicate), got %d", n)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestCommentTriggerCoalescing verifies that rapid-fire comments don't create
|
||||
// duplicate tasks (coalescing dedup).
|
||||
func TestCommentTriggerCoalescing(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -188,7 +188,8 @@ func (h *Handler) CreateComment(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
// Trigger @mentioned agents: parse agent mentions and enqueue tasks for each.
|
||||
h.enqueueMentionedAgentTasks(r.Context(), issue, comment, authorType, authorID)
|
||||
// Pass parentComment so that replies inherit mentions from the thread root.
|
||||
h.enqueueMentionedAgentTasks(r.Context(), issue, comment, parentComment, authorType, authorID)
|
||||
|
||||
writeJSON(w, http.StatusCreated, resp)
|
||||
}
|
||||
|
|
@ -248,16 +249,34 @@ func (h *Handler) isReplyToMemberThread(parent *db.Comment, content string, issu
|
|||
}
|
||||
|
||||
// enqueueMentionedAgentTasks parses @agent mentions from comment content and
|
||||
// enqueues a task for each mentioned agent. Skips self-mentions, agents that
|
||||
// are already the issue's assignee (handled by on_comment), agents with
|
||||
// on_mention trigger disabled, and private agents mentioned by non-owner
|
||||
// members (only the agent owner or workspace admin/owner can mention a
|
||||
// private agent).
|
||||
// enqueues a task for each mentioned agent. When parentComment is non-nil
|
||||
// (i.e. the comment is a reply), mentions from the parent (thread root) are
|
||||
// also included so that agents mentioned in the top-level comment are
|
||||
// re-triggered by subsequent replies in the same thread.
|
||||
// Skips self-mentions, agents that are already the issue's assignee (handled
|
||||
// by on_comment), agents with on_mention trigger disabled, and private agents
|
||||
// mentioned by non-owner members (only the agent owner or workspace
|
||||
// admin/owner can mention a private agent).
|
||||
// Note: no status gate here — @mention is an explicit action and should work
|
||||
// even on done/cancelled issues (the agent can reopen the issue if needed).
|
||||
func (h *Handler) enqueueMentionedAgentTasks(ctx context.Context, issue db.Issue, comment db.Comment, authorType, authorID string) {
|
||||
func (h *Handler) enqueueMentionedAgentTasks(ctx context.Context, issue db.Issue, comment db.Comment, parentComment *db.Comment, authorType, authorID string) {
|
||||
wsID := uuidToString(issue.WorkspaceID)
|
||||
mentions := util.ParseMentions(comment.Content)
|
||||
// When replying in a thread, also include mentions from the parent comment
|
||||
// so that agents mentioned in the thread root are triggered by replies.
|
||||
if parentComment != nil {
|
||||
parentMentions := util.ParseMentions(parentComment.Content)
|
||||
seen := make(map[string]bool, len(mentions))
|
||||
for _, m := range mentions {
|
||||
seen[m.Type+":"+m.ID] = true
|
||||
}
|
||||
for _, m := range parentMentions {
|
||||
if !seen[m.Type+":"+m.ID] {
|
||||
mentions = append(mentions, m)
|
||||
seen[m.Type+":"+m.ID] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, m := range mentions {
|
||||
if m.Type != "agent" {
|
||||
continue
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue