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
This commit is contained in:
viettranx committed 2026-03-17 18:03:10 +07:00
1 parent b735c16d93
commit b231878a85
8 files changed
+73 -15

No files matched your search

+1 -1
View File
@@ -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 ""
}
+4 -4
View File
@@ -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
+3 -3
View File
@@ -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
+8 -1
View File
@@ -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),
+48 -3
View File
@@ -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, &params)
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(),
+6 -2
View File
@@ -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
}
+2 -1
View File
@@ -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.
+1
View File
@@ -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"