fix(teams): propagate peer_kind and local_key through task dispatch chain

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
This commit is contained in:
viettranx committed 2026-03-16 09:01:13 +07:00
1 parent 0857321a6b
commit 0dc3124607
5 files changed
+54 -8

No files matched your search

+11 -2
View File
@@ -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",
+2 -1
View File
@@ -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=<question>. 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: <feedback>**.\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()
+13 -2
View File
@@ -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{
+9 -1
View File
@@ -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 {
+19 -2
View File
@@ -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.