From b231878a8545ea0760616fb0348baa454934be0d Mon Sep 17 00:00:00 2001 From: viettranx Date: Tue, 17 Mar 2026 18:03:10 +0700 Subject: [PATCH] feat(teams): add limit param to ListTasks + lightweight get-light endpoint - Add limit parameter to ListTasks interface (dashboard=200, agent=30) - Add teams.tasks.get-light WS method returning task only (no comments/events) - Truncate dashboard response to exact limit (fix off-by-one from limit+1) - Update all 7 ListTasks callers with explicit limit values --- cmd/gateway_consumer_handlers.go | 2 +- internal/agent/loop.go | 8 +-- internal/channels/telegram/commands_tasks.go | 6 +-- internal/gateway/methods/teams_crud.go | 9 +++- internal/gateway/methods/teams_tasks.go | 51 ++++++++++++++++++-- internal/store/pg/teams_tasks.go | 8 ++- internal/store/team_store.go | 3 +- pkg/protocol/methods.go | 1 + 8 files changed, 73 insertions(+), 15 deletions(-) diff --git a/cmd/gateway_consumer_handlers.go b/cmd/gateway_consumer_handlers.go index 4435e3c2..43eeb1b9 100644 --- a/cmd/gateway_consumer_handlers.go +++ b/cmd/gateway_consumer_handlers.go @@ -763,7 +763,7 @@ func buildTaskBoardSnapshot(ctx context.Context, teamStore store.TeamStore, team if team, err := teamStore.GetTeam(ctx, teamID); err == nil && tools.IsSharedWorkspace(team.Settings) { snapshotChatID = "" } - allTasks, err := teamStore.ListTasks(ctx, teamID, "", store.TeamTaskFilterAll, "", "", snapshotChatID, 0) + allTasks, err := teamStore.ListTasks(ctx, teamID, "", store.TeamTaskFilterAll, "", "", snapshotChatID, 0, 0) if err != nil || len(allTasks) == 0 { return "" } diff --git a/internal/agent/loop.go b/internal/agent/loop.go index f2794366..6095d4a2 100644 --- a/internal/agent/loop.go +++ b/internal/agent/loop.go @@ -359,7 +359,7 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error) // Stale recovery (expired lock → pending) is handled by the background TaskTicker. 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", "active", req.UserID, "", "", 0); err == nil { + if tasks, err := l.teamStore.ListTasks(ctx, team.ID, "newest", "active", req.UserID, "", "", 0, 0); err == nil { var stale []string var inProgress []string for _, t := range tasks { @@ -465,7 +465,7 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error) var skillNudge70Sent, skillNudge90Sent bool var skillPostscriptSent bool - // Member progress nudge: remind dispatched members to report progress (every 10 iterations). + // Member progress nudge: remind dispatched members to report progress (every 6 iterations). // Inject retry hook so channels can update placeholder on LLM retries. ctx = providers.WithRetryHook(ctx, func(attempt, maxAttempts int, err error) { @@ -524,9 +524,9 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error) } } - // Member progress nudge: remind to report progress every 10 iterations. + // Member progress nudge: remind to report progress every 6 iterations. // Suggests percent based on iteration ratio — model can adjust but has a baseline. - if req.TeamTaskID != "" && memberTaskSubject != "" && iteration > 0 && iteration%10 == 0 { + if req.TeamTaskID != "" && memberTaskSubject != "" && iteration > 0 && iteration%6 == 0 { var nudge string if maxIter > 0 { suggestedPct := iteration * 100 / maxIter diff --git a/internal/channels/telegram/commands_tasks.go b/internal/channels/telegram/commands_tasks.go index 457fbb89..fa9883a1 100644 --- a/internal/channels/telegram/commands_tasks.go +++ b/internal/channels/telegram/commands_tasks.go @@ -81,7 +81,7 @@ func (c *Channel) handleTasksList(ctx context.Context, chatID int64, isGroup boo return } - tasks, err := c.teamStore.ListTasks(ctx, team.ID, "newest", store.TeamTaskFilterAll, taskUserID(c.Name(), chatID, isGroup), "", "", 0) + tasks, err := c.teamStore.ListTasks(ctx, team.ID, "newest", store.TeamTaskFilterAll, taskUserID(c.Name(), chatID, isGroup), "", "", 0, 0) if err != nil { slog.Warn("tasks command: ListTasks failed", "error", err) send("Failed to list tasks. Please try again.") @@ -173,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, taskUserID(c.Name(), chatID, isGroup), "", "", 0) + tasks, err := c.teamStore.ListTasks(ctx, team.ID, "newest", store.TeamTaskFilterAll, taskUserID(c.Name(), chatID, isGroup), "", "", 0, 0) if err != nil { slog.Warn("task_detail command: ListTasks failed", "error", err) send("Failed to list tasks. Please try again.") @@ -236,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, taskUserID(c.Name(), chatID, isGroup), "", "", 0) + tasks, err := c.teamStore.ListTasks(ctx, team.ID, "newest", store.TeamTaskFilterAll, taskUserID(c.Name(), chatID, isGroup), "", "", 0, 0) if err != nil { send("Failed to list tasks.") return diff --git a/internal/gateway/methods/teams_crud.go b/internal/gateway/methods/teams_crud.go index 52ce4bf9..00753a28 100644 --- a/internal/gateway/methods/teams_crud.go +++ b/internal/gateway/methods/teams_crud.go @@ -156,13 +156,20 @@ func (m *TeamsMethods) handleTaskList(ctx context.Context, client *gateway.Clien return } - tasks, err := m.teamStore.ListTasks(ctx, teamID, "newest", params.Status, "", params.Channel, params.ChatID, 0) + const dashboardLimit = 200 + tasks, err := m.teamStore.ListTasks(ctx, teamID, "newest", params.Status, "", params.Channel, params.ChatID, dashboardLimit, 0) if err != nil { slog.Warn("teams.tasks.list failed", "team_id", teamID, "status_filter", params.Status, "error", err) client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, err.Error())) return } + // ListTasks returns limit+1 rows for hasMore detection; truncate for dashboard response. + hasMore := len(tasks) > dashboardLimit + if hasMore { + tasks = tasks[:dashboardLimit] + } + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ "tasks": tasks, "count": len(tasks), diff --git a/internal/gateway/methods/teams_tasks.go b/internal/gateway/methods/teams_tasks.go index 86e62853..6dbb62fb 100644 --- a/internal/gateway/methods/teams_tasks.go +++ b/internal/gateway/methods/teams_tasks.go @@ -46,6 +46,7 @@ func (m *TeamsMethods) parseTaskParams(ctx context.Context, client *gateway.Clie // RegisterTasks registers teams.tasks.* RPC handlers. func (m *TeamsMethods) RegisterTasks(router *gateway.MethodRouter) { router.Register(protocol.MethodTeamsTaskGet, m.handleTaskGet) + router.Register(protocol.MethodTeamsTaskGetLight, m.handleTaskGetLight) router.Register(protocol.MethodTeamsTaskApprove, m.handleTaskApprove) router.Register(protocol.MethodTeamsTaskReject, m.handleTaskReject) router.Register(protocol.MethodTeamsTaskComment, m.handleTaskComment) @@ -111,6 +112,47 @@ func (m *TeamsMethods) handleTaskGet(ctx context.Context, client *gateway.Client })) } +// --- Task Get Light (task only, no comments/events/attachments) --- + +func (m *TeamsMethods) handleTaskGetLight(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) { + var params taskGetParams + locale, ok := m.parseTaskParams(ctx, client, req, ¶ms) + if !ok { + return + } + + teamID, err := uuid.Parse(params.TeamID) + if err != nil { + client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidID, "teamId"))) + return + } + taskID, err := uuid.Parse(params.TaskID) + if err != nil { + client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidID, "taskId"))) + return + } + + task, err := m.teamStore.GetTask(ctx, taskID) + if err != nil { + if errors.Is(err, store.ErrTaskNotFound) { + client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrNotFound, i18n.T(locale, i18n.MsgNotFound, "task", ""))) + } else { + slog.Warn("teams.tasks.get-light failed", "task_id", taskID, "error", err) + client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgInternalError, ""))) + } + return + } + + if task.TeamID != teamID { + client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrNotFound, i18n.T(locale, i18n.MsgNotFound, "task", ""))) + return + } + + client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{ + "task": task, + })) +} + // --- Task Approve --- type taskApproveParams struct { @@ -487,7 +529,8 @@ func (m *TeamsMethods) handleTaskCreate(ctx context.Context, client *gateway.Cli TaskID: task.ID.String(), Status: store.TeamTaskStatusPending, UserID: client.UserID(), - Channel: "dashboard", + Channel: ch, + ChatID: cid, Timestamp: taskNowUTC(), ActorType: "human", ActorID: client.UserID(), @@ -500,7 +543,8 @@ func (m *TeamsMethods) handleTaskCreate(ctx context.Context, client *gateway.Cli Status: store.TeamTaskStatusInProgress, OwnerAgentKey: autoAssignedAgentID.String(), UserID: client.UserID(), - Channel: "dashboard", + Channel: ch, + ChatID: cid, Timestamp: taskNowUTC(), ActorType: "human", ActorID: client.UserID(), @@ -573,7 +617,8 @@ func (m *TeamsMethods) handleTaskAssign(ctx context.Context, client *gateway.Cli TaskID: taskID.String(), Status: store.TeamTaskStatusInProgress, UserID: client.UserID(), - Channel: "dashboard", + Channel: task.Channel, + ChatID: task.ChatID, Timestamp: taskNowUTC(), ActorType: "human", ActorID: client.UserID(), diff --git a/internal/store/pg/teams_tasks.go b/internal/store/pg/teams_tasks.go index 1af1ec87..78b602e1 100644 --- a/internal/store/pg/teams_tasks.go +++ b/internal/store/pg/teams_tasks.go @@ -163,7 +163,7 @@ func (s *PGTeamStore) UpdateTask(ctx context.Context, taskID uuid.UUID, updates return execMapUpdate(ctx, s.db, "team_tasks", taskID, updates) } -func (s *PGTeamStore) ListTasks(ctx context.Context, teamID uuid.UUID, orderBy string, statusFilter string, userID string, channel string, chatID string, offset int) ([]store.TeamTaskData, error) { +func (s *PGTeamStore) ListTasks(ctx context.Context, teamID uuid.UUID, orderBy string, statusFilter string, userID string, channel string, chatID string, limit int, offset int) ([]store.TeamTaskData, error) { orderClause := "t.priority DESC, t.created_at" if orderBy == "newest" { orderClause = "t.created_at DESC" @@ -180,6 +180,10 @@ func (s *PGTeamStore) ListTasks(ctx context.Context, teamID uuid.UUID, orderBy s // "", store.TeamTaskFilterAll ("all") → no filter (all statuses) } + if limit <= 0 { + limit = maxListTasksRows + } + // Scope filter: always bind $4/$5 but only enforce when non-empty. scopeWhere := "AND ($4 = '' OR COALESCE(t.channel,'') = $4) AND ($5 = '' OR COALESCE(t.chat_id,'') = $5)" @@ -188,7 +192,7 @@ func (s *PGTeamStore) ListTasks(ctx context.Context, teamID uuid.UUID, orderBy s `+taskJoinClause+` WHERE t.team_id = $1 AND ($2 = '' OR t.user_id = $2) `+statusWhere+` `+scopeWhere+` ORDER BY `+orderClause+` - LIMIT $3 OFFSET $6`, teamID, userID, maxListTasksRows+1, channel, chatID, offset) + LIMIT $3 OFFSET $6`, teamID, userID, limit+1, channel, chatID, offset) if err != nil { return nil, err } diff --git a/internal/store/team_store.go b/internal/store/team_store.go index 98944a18..0658cad2 100644 --- a/internal/store/team_store.go +++ b/internal/store/team_store.go @@ -316,7 +316,8 @@ type TeamStore interface { // statusFilter: "" = non-completed (default), "completed", "all". // userID: if non-empty, filter to tasks created by this user. // channel+chatID: if either is non-empty, filter to that exact scope. - ListTasks(ctx context.Context, teamID uuid.UUID, orderBy string, statusFilter string, userID string, channel string, chatID string, offset int) ([]TeamTaskData, error) + // limit: max rows to return; 0 uses the default (30). + ListTasks(ctx context.Context, teamID uuid.UUID, orderBy string, statusFilter string, userID string, channel string, chatID string, limit int, offset int) ([]TeamTaskData, error) // GetTask returns a single task by ID with joined agent info. GetTask(ctx context.Context, taskID uuid.UUID) (*TeamTaskData, error) // GetTasksByIDs returns multiple tasks by IDs in a single query. diff --git a/pkg/protocol/methods.go b/pkg/protocol/methods.go index cefaa168..6f160bed 100644 --- a/pkg/protocol/methods.go +++ b/pkg/protocol/methods.go @@ -108,6 +108,7 @@ const ( MethodTeamsDelete = "teams.delete" MethodTeamsTaskList = "teams.tasks.list" MethodTeamsTaskGet = "teams.tasks.get" + MethodTeamsTaskGetLight = "teams.tasks.get-light" MethodTeamsTaskApprove = "teams.tasks.approve" MethodTeamsTaskReject = "teams.tasks.reject" MethodTeamsTaskComment = "teams.tasks.comment"