From 0dc31246072ded47703691e24232e3b4cda095d8 Mon Sep 17 00:00:00 2001 From: viettranx Date: Mon, 16 Mar 2026 09:01:13 +0700 Subject: [PATCH] fix(teams): propagate peer_kind and local_key through task dispatch chain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Team task announce was writing to wrong session (direct instead of group) because origin_peer_kind was hardcoded as "direct" in dispatch metadata. This caused leaders to miss completed task results in group conversations. - Store peer_kind and local_key in task metadata at creation time - Resolve peer_kind from context → metadata → "direct" fallback in all dispatch paths (tool, gateway, unblocked) - Use actual origPeerKind in announce handler session key + request - Add origin_local_key to gateway dispatch for forum topic routing - Clarify ask_user guidance: bot must present question directly - Guide members to use team_tasks progress instead of team_message - Improve error message when non-owner calls progress action --- cmd/gateway_consumer_handlers.go | 13 +++++++++++-- internal/agent/resolver_helpers.go | 3 ++- internal/gateway/methods/teams_tasks.go | 15 +++++++++++++-- internal/tools/team_tasks_mutations.go | 10 +++++++++- internal/tools/team_tool_dispatch.go | 21 +++++++++++++++++++-- 5 files changed, 54 insertions(+), 8 deletions(-) diff --git a/cmd/gateway_consumer_handlers.go b/cmd/gateway_consumer_handlers.go index 6798bddd..52ca6cd0 100644 --- a/cmd/gateway_consumer_handlers.go +++ b/cmd/gateway_consumer_handlers.go @@ -660,14 +660,23 @@ func handleTeammateMessage( slog.Warn("teammate announce: no origin_chat_id, cannot announce to lead") return } - leadSessionKey := sessions.BuildScopedSessionKey(leadAgent, origCh, sessions.PeerDirect, origChatID, cfg.Sessions.Scope, cfg.Sessions.DmScope, cfg.Sessions.MainKey) + // Use the original peer kind so announce writes to the correct session + // (group session for group chats, direct session for DMs). + origPeerKind := inMeta["origin_peer_kind"] + if origPeerKind == "" { + origPeerKind = string(sessions.PeerDirect) + } + origLocalKey := inMeta["origin_local_key"] + leadSessionKey := sessions.BuildScopedSessionKey(leadAgent, origCh, sessions.PeerKind(origPeerKind), origChatID, cfg.Sessions.Scope, cfg.Sessions.DmScope, cfg.Sessions.MainKey) + leadSessionKey = overrideSessionKeyFromLocalKey(leadSessionKey, origLocalKey, leadAgent, origCh, origChatID, origPeerKind) announceReq := agent.RunRequest{ SessionKey: leadSessionKey, Message: announceContent, Channel: origCh, ChatID: origChatID, - PeerKind: string(sessions.PeerDirect), + PeerKind: origPeerKind, + LocalKey: origLocalKey, UserID: inMeta["origin_user_id"], RunID: fmt.Sprintf("teammate-announce-%s", memberAgent), RunKind: "announce", diff --git a/internal/agent/resolver_helpers.go b/internal/agent/resolver_helpers.go index d0f6a855..0f7f87f5 100644 --- a/internal/agent/resolver_helpers.go +++ b/internal/agent/resolver_helpers.go @@ -116,6 +116,7 @@ func buildTeamMD(team *store.TeamData, members []store.TeamMemberData, selfID uu sb.WriteString("\n## Follow-up Reminders\n\n") sb.WriteString("When you need user input/decision: create+claim task, then `ask_user` with text=. ONLY use when you have a question for the user — NOT for waiting on teammates or status updates.\n") + sb.WriteString("IMPORTANT: Present the question directly to the user in your response. `ask_user` only sets up periodic REMINDERS in case they don't reply — it does NOT present the question for you.\n") sb.WriteString("System auto-sends reminders. Call `clear_ask_user` when user replies.\n") } else { sb.WriteString("Create a task with `team_tasks` (with `assignee`), then the system dispatches automatically.\n") @@ -133,7 +134,7 @@ func buildTeamMD(team *store.TeamData, members []store.TeamMemberData, selfID uu sb.WriteString("You are a **reviewer**. When evaluating, respond with **APPROVED** or **REJECTED: **.\n\n") } sb.WriteString("As a member, just do the assigned work. Task completion is automatic.\n") - sb.WriteString("For long-running tasks, send progress updates via `team_message` action=send.\n") + sb.WriteString("For long-running tasks, use `team_tasks(action=\"progress\", percent=50, text=\"status update\")` to report progress. The task_id is auto-resolved from your assigned task — you don't need to specify it.\n") } return sb.String() diff --git a/internal/gateway/methods/teams_tasks.go b/internal/gateway/methods/teams_tasks.go index 1d1e352f..30073cee 100644 --- a/internal/gateway/methods/teams_tasks.go +++ b/internal/gateway/methods/teams_tasks.go @@ -680,20 +680,31 @@ func (m *TeamsMethods) dispatchTaskToAgent(ctx context.Context, task *store.Team } } + // Resolve peer kind from task metadata; fallback to "direct" for old tasks. + originPeerKind := "direct" + if task.Metadata != nil { + if pk, ok := task.Metadata["peer_kind"].(string); ok && pk != "" { + originPeerKind = pk + } + } + meta := map[string]string{ "origin_channel": originChannel, - "origin_peer_kind": "direct", + "origin_peer_kind": originPeerKind, "origin_chat_id": task.ChatID, "from_agent": fromAgent, "to_agent": ag.AgentKey, "team_task_id": taskID.String(), "team_id": teamID.String(), } - // Pass team workspace from task metadata so member uses the team's workspace. + // Pass team workspace and local key from task metadata. if task.Metadata != nil { if ws, _ := task.Metadata["team_workspace"].(string); ws != "" { meta["team_workspace"] = ws } + if lk, _ := task.Metadata["local_key"].(string); lk != "" { + meta["origin_local_key"] = lk + } } m.msgBus.PublishInbound(bus.InboundMessage{ diff --git a/internal/tools/team_tasks_mutations.go b/internal/tools/team_tasks_mutations.go index 308ba762..a700c469 100644 --- a/internal/tools/team_tasks_mutations.go +++ b/internal/tools/team_tasks_mutations.go @@ -117,6 +117,14 @@ func (t *TeamTasksTool) executeCreate(ctx context.Context, args map[string]any) } taskMeta["original_blocked_by"] = ids } + // Store peer kind so dispatches preserve the correct session scope (group vs direct). + if pk := ToolPeerKindFromCtx(ctx); pk != "" { + taskMeta["peer_kind"] = pk + } + // Store local key so forum-topic routing works on deferred/unblocked dispatches. + if lk := ToolLocalKeyFromCtx(ctx); lk != "" { + taskMeta["local_key"] = lk + } // Store leader's trace context so unblocked dispatch links back to the leader's trace. if traceID := tracing.TraceIDFromContext(ctx); traceID != uuid.Nil { taskMeta["origin_trace_id"] = traceID.String() @@ -278,7 +286,7 @@ func (t *TeamTasksTool) executeProgress(ctx context.Context, args map[string]any return ErrorResult("task does not belong to your team") } if task.OwnerAgentID == nil || *task.OwnerAgentID != agentID { - return ErrorResult("only the task owner can update progress") + return ErrorResult("only the assigned task owner can update progress. As team lead, task results arrive automatically when members complete their work.") } if err := t.manager.teamStore.UpdateTaskProgress(ctx, taskID, team.ID, percent, step); err != nil { diff --git a/internal/tools/team_tool_dispatch.go b/internal/tools/team_tool_dispatch.go index 95512186..b12ad8f2 100644 --- a/internal/tools/team_tool_dispatch.go +++ b/internal/tools/team_tool_dispatch.go @@ -95,9 +95,19 @@ func (m *TeamToolManager) dispatchTaskToAgent(ctx context.Context, task *store.T originUserID = originChatID } + // Resolve peer kind from context; fallback to task metadata, then "direct". + originPeerKind := ToolPeerKindFromCtx(ctx) + if originPeerKind == "" { + if pk, ok := task.Metadata["peer_kind"].(string); ok && pk != "" { + originPeerKind = pk + } else { + originPeerKind = "direct" + } + } + meta := map[string]string{ "origin_channel": originChannel, - "origin_peer_kind": "direct", + "origin_peer_kind": originPeerKind, "origin_chat_id": originChatID, "origin_user_id": originUserID, "from_agent": fromAgent, @@ -105,7 +115,14 @@ func (m *TeamToolManager) dispatchTaskToAgent(ctx context.Context, task *store.T "team_task_id": task.ID.String(), "team_id": teamID.String(), } - if localKey := ToolLocalKeyFromCtx(ctx); localKey != "" { + // Resolve local key from context; fallback to task metadata for deferred dispatches. + localKey := ToolLocalKeyFromCtx(ctx) + if localKey == "" { + if lk, ok := task.Metadata["local_key"].(string); ok { + localKey = lk + } + } + if localKey != "" { meta["origin_local_key"] = localKey } // Pass the team workspace dir so member agents write files to the shared folder.