mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-03 19:20:07 +02:00
fix(daemon): reply under the specific mentioning comment per thread (MUL-4348 review nit #1)
Non-trigger threads previously replied under the thread root, while the trigger's thread replied under the trigger comment — asymmetric, and it put the answer at the top of the thread instead of next to the actual question when the mention was a mid-thread reply. Reply under the NEWEST triggering comment in each thread instead (inputs are chronological, so last-write-wins per thread), making every thread consistent and nesting each answer beside its question. Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
@@ -241,11 +241,14 @@ func buildCommentPrompt(task Task, provider string) string {
|
||||
// threads yield one group each so the agent replies inside each thread instead
|
||||
// of merging them into one blob (MUL-4348).
|
||||
//
|
||||
// The trigger's own thread replies under the trigger comment itself, matching
|
||||
// the long-standing single-thread behaviour (--parent = trigger comment id);
|
||||
// every other thread replies under its root comment id. Returns nil when there
|
||||
// is no trigger or only a single distinct thread — the caller then keeps the
|
||||
// existing single-parent reply path unchanged.
|
||||
// The reply for each thread targets the NEWEST comment that triggered this run
|
||||
// in that thread (coalesced comments arrive oldest-first and the trigger is the
|
||||
// newest overall, so a simple last-write-wins yields the newest per thread).
|
||||
// That nests the answer next to the most recent question in the thread rather
|
||||
// than at the thread root, and makes the trigger's own thread (--parent =
|
||||
// trigger comment) consistent with every other thread instead of a special
|
||||
// case. Returns nil when there is no trigger or only a single distinct thread —
|
||||
// the caller then keeps the existing single-parent reply path unchanged.
|
||||
func commentReplyThreads(task Task) []execenv.ThreadReplyTarget {
|
||||
if task.TriggerCommentID == "" {
|
||||
return nil
|
||||
@@ -261,6 +264,9 @@ func commentReplyThreads(task Task) []execenv.ThreadReplyTarget {
|
||||
|
||||
order := make([]string, 0, len(task.CoalescedComments)+1)
|
||||
parentByThread := make(map[string]string, len(task.CoalescedComments)+1)
|
||||
// note records first-seen order but lets the newest comment win the reply
|
||||
// target: inputs are chronological (coalesced oldest-first, trigger last),
|
||||
// so the last write for a thread is its newest triggering comment.
|
||||
note := func(threadID, parentID string) {
|
||||
if _, ok := parentByThread[threadID]; !ok {
|
||||
order = append(order, threadID)
|
||||
@@ -268,16 +274,14 @@ func commentReplyThreads(task Task) []execenv.ThreadReplyTarget {
|
||||
parentByThread[threadID] = parentID
|
||||
}
|
||||
|
||||
// Coalesced (older) comments first: each replies under its own thread root.
|
||||
// Coalesced (older) comments first: reply under the specific comment that
|
||||
// mentioned the agent, not the thread root, so a mid-thread mention gets its
|
||||
// answer next to the question.
|
||||
for _, cc := range task.CoalescedComments {
|
||||
tid := threadKey(cc.ThreadID, cc.ID)
|
||||
if _, ok := parentByThread[tid]; ok {
|
||||
continue // thread already represented; keep its first parent
|
||||
}
|
||||
note(tid, tid)
|
||||
note(threadKey(cc.ThreadID, cc.ID), cc.ID)
|
||||
}
|
||||
// The newest trigger last: its thread replies under the trigger comment,
|
||||
// overriding any coalesced comment that shared the trigger's thread.
|
||||
// The newest trigger last: it always wins its own thread's reply target,
|
||||
// overriding any earlier coalesced comment that shared the trigger's thread.
|
||||
note(threadKey(task.TriggerThreadID, task.TriggerCommentID), task.TriggerCommentID)
|
||||
|
||||
if len(order) <= 1 {
|
||||
|
||||
@@ -741,8 +741,11 @@ func TestCommentReplyThreadsGrouping(t *testing.T) {
|
||||
if got["thread-A"] != "c3" {
|
||||
t.Errorf("trigger thread parent = %q, want c3 (the trigger comment)", got["thread-A"])
|
||||
}
|
||||
if got["thread-B"] != "thread-B" {
|
||||
t.Errorf("other thread parent = %q, want its root thread-B", got["thread-B"])
|
||||
// The other thread replies under the specific comment that mentioned the
|
||||
// agent (a mid-thread reply), not the thread root — fixes the placement
|
||||
// asymmetry from the first cut.
|
||||
if got["thread-B"] != "c2" {
|
||||
t.Errorf("other thread parent = %q, want c2 (the specific mentioning comment)", got["thread-B"])
|
||||
}
|
||||
})
|
||||
|
||||
@@ -752,6 +755,30 @@ func TestCommentReplyThreadsGrouping(t *testing.T) {
|
||||
t.Fatalf("ordinary single-comment run must not fan out; got %+v", targets)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("non-trigger thread replies under its newest mention, not root", func(t *testing.T) {
|
||||
// Two mid-thread mentions in thread-B (oldest c1, newer c2); the reply
|
||||
// should target the newest specific comment (c2), not the root thread-B.
|
||||
task := Task{
|
||||
TriggerCommentID: "c9",
|
||||
TriggerThreadID: "thread-A",
|
||||
CoalescedComments: []CoalescedCommentData{
|
||||
{ID: "c1", ThreadID: "thread-B", Content: "older mention", CreatedAt: "2026-07-10T01:00:00Z"},
|
||||
{ID: "c2", ThreadID: "thread-B", Content: "newer mention", CreatedAt: "2026-07-10T02:00:00Z"},
|
||||
},
|
||||
}
|
||||
targets := commentReplyThreads(task)
|
||||
got := map[string]string{}
|
||||
for _, tgt := range targets {
|
||||
got[tgt.ThreadID] = tgt.ParentID
|
||||
}
|
||||
if got["thread-B"] != "c2" {
|
||||
t.Errorf("thread-B parent = %q, want newest mention c2 (not root)", got["thread-B"])
|
||||
}
|
||||
if got["thread-A"] != "c9" {
|
||||
t.Errorf("trigger thread parent = %q, want trigger c9", got["thread-A"])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestBuildCommentPromptCrossThreadFansOutReplies is the end-to-end prompt
|
||||
|
||||
Reference in New Issue
Block a user