fix(teams): scope ListTasks by userID to prevent cross-group task leaking

Tasks from one Telegram group were being injected into another group's
session because the pending-task hint and /tasks command queried all
tasks team-wide without filtering by the group-scoped userID.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
viettranxandClaude Opus 4.6 committed 2026-03-03 19:27:23 +07:00
1 parent 9b12f0adfd
commit 361bca3617
4 files changed
+22 -11

No files matched your search

+1 -1
View File
@@ -542,7 +542,7 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error)
// so only truly un-spawned tasks remain pending.
if l.teamStore != nil && l.agentUUID != uuid.Nil {
if team, _ := l.teamStore.GetTeamForAgent(ctx, l.agentUUID); team != nil && team.LeadAgentID == l.agentUUID {
if tasks, err := l.teamStore.ListTasks(ctx, team.ID, "newest", "", ""); err == nil {
if tasks, err := l.teamStore.ListTasks(ctx, team.ID, "newest", "", req.UserID); err == nil {
var stale []string
for _, t := range tasks {
if t.Status == store.TeamTaskStatusPending {
+2 -2
View File
@@ -190,11 +190,11 @@ func (c *Channel) handleBotCommand(ctx context.Context, message *telego.Message,
return true
case "/tasks":
c.handleTasksList(ctx, chatID, setThread)
c.handleTasksList(ctx, chatID, isGroup, setThread)
return true
case "/task_detail":
c.handleTaskDetail(ctx, chatID, text, setThread)
c.handleTaskDetail(ctx, chatID, text, isGroup, setThread)
return true
case "/addwriter":
+18 -7
View File
@@ -39,8 +39,17 @@ func truncateStr(s string, maxLen int) string {
return string(runes[:maxLen]) + "…"
}
// taskUserID composes the scoped user ID for task filtering.
// Groups use "group:{channel}:{chatID}", DMs use the chat ID directly.
func taskUserID(channelName string, chatID int64, isGroup bool) string {
if isGroup {
return fmt.Sprintf("group:%s:%d", channelName, chatID)
}
return fmt.Sprintf("%d", chatID)
}
// handleTasksList handles the /tasks command — lists team tasks.
func (c *Channel) handleTasksList(ctx context.Context, chatID int64, setThread func(*telego.SendMessageParams)) {
func (c *Channel) handleTasksList(ctx context.Context, chatID int64, isGroup bool, setThread func(*telego.SendMessageParams)) {
chatIDObj := tu.ID(chatID)
send := func(text string) {
@@ -72,7 +81,7 @@ func (c *Channel) handleTasksList(ctx context.Context, chatID int64, setThread f
return
}
tasks, err := c.teamStore.ListTasks(ctx, team.ID, "newest", store.TeamTaskFilterAll, "")
tasks, err := c.teamStore.ListTasks(ctx, team.ID, "newest", store.TeamTaskFilterAll, taskUserID(c.Name(), chatID, isGroup))
if err != nil {
slog.Warn("tasks command: ListTasks failed", "error", err)
send("Failed to list tasks. Please try again.")
@@ -122,7 +131,7 @@ func (c *Channel) handleTasksList(ctx context.Context, chatID int64, setThread f
}
// handleTaskDetail handles the /task_detail command — shows detail for a task.
func (c *Channel) handleTaskDetail(ctx context.Context, chatID int64, text string, setThread func(*telego.SendMessageParams)) {
func (c *Channel) handleTaskDetail(ctx context.Context, chatID int64, text string, isGroup bool, setThread func(*telego.SendMessageParams)) {
chatIDObj := tu.ID(chatID)
send := func(t string) {
@@ -164,7 +173,7 @@ func (c *Channel) handleTaskDetail(ctx context.Context, chatID int64, text strin
return
}
tasks, err := c.teamStore.ListTasks(ctx, team.ID, "newest", store.TeamTaskFilterAll, "")
tasks, err := c.teamStore.ListTasks(ctx, team.ID, "newest", store.TeamTaskFilterAll, taskUserID(c.Name(), chatID, isGroup))
if err != nil {
slog.Warn("task_detail command: ListTasks failed", "error", err)
send("Failed to list tasks. Please try again.")
@@ -197,9 +206,11 @@ func (c *Channel) handleCallbackQuery(ctx context.Context, query *telego.Callbac
taskIDStr := strings.TrimPrefix(query.Data, "td:")
// Resolve chat ID from the callback's message.
chatID := query.Message.GetChat().ID
// Resolve chat ID and group status from the callback's message.
chat := query.Message.GetChat()
chatID := chat.ID
chatIDObj := tu.ID(chatID)
isGroup := chat.Type == "group" || chat.Type == "supergroup"
send := func(text string) {
for _, chunk := range chunkPlainText(text, telegramMaxMessageLen) {
@@ -225,7 +236,7 @@ func (c *Channel) handleCallbackQuery(ctx context.Context, query *telego.Callbac
return
}
tasks, err := c.teamStore.ListTasks(ctx, team.ID, "newest", store.TeamTaskFilterAll, "")
tasks, err := c.teamStore.ListTasks(ctx, team.ID, "newest", store.TeamTaskFilterAll, taskUserID(c.Name(), chatID, isGroup))
if err != nil {
send("Failed to list tasks.")
return
+1 -1
View File
@@ -503,7 +503,7 @@ func buildDelegateMessage(opts DelegateOpts) string {
// pendingTasksHint returns a formatted hint listing pending team tasks,
// so the LLM can self-correct a hallucinated team_task_id.
func (dm *DelegateManager) pendingTasksHint(ctx context.Context, teamID uuid.UUID) string {
tasks, err := dm.teamStore.ListTasks(ctx, teamID, "newest", "", "")
tasks, err := dm.teamStore.ListTasks(ctx, teamID, "newest", "", store.UserIDFromContext(ctx))
if err != nil {
return ""
}