From 961de18c97173beefb077db6715d94b2fde66065 Mon Sep 17 00:00:00 2001 From: LinYushen Date: Tue, 31 Mar 2026 13:48:39 +0800 Subject: [PATCH] feat(agents): reply as thread instead of top-level comment (#205) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(agents): reply as thread instead of top-level comment When an agent responds to a user comment, the reply is now nested under the triggering comment (parent_id) instead of appearing as a separate top-level comment. Also enables on_comment trigger by default for newly created agents. - Add trigger_comment_id column to agent_task_queue (migration 028) - Pass triggering comment ID through EnqueueTaskForIssue → task → createAgentComment - Include parent_id in WebSocket broadcast for agent comments - Default agent creation includes both on_assign and on_comment triggers Co-Authored-By: Claude Opus 4.6 (1M context) * feat(cli): add --parent flag to comment add for threaded replies The agent posts comments via the CLI, so the correct fix is giving it a --parent flag rather than wiring trigger_comment_id through the task infrastructure. The agent reads the comment list, decides which comment to reply to, and passes --parent . - Add --parent flag to `multica issue comment add` - Update agent runtime instructions to explain --parent usage Co-Authored-By: Claude Opus 4.6 (1M context) * feat(daemon): pass trigger_comment_id to agent execution context The agent now knows which comment triggered its task and gets an explicit instruction to reply to it using --parent. The trigger_comment_id flows from the DB through the claim response, daemon Task struct, and into issue_context.md where the agent sees it. Co-Authored-By: Claude Opus 4.6 (1M context) * fix(comments): agent replies to thread root, matching frontend behavior When the triggering comment is itself a reply (has parent_id), resolve to the thread root so the agent's reply stays in the same flat thread. This matches the frontend where all replies share the top-level parent. Co-Authored-By: Claude Opus 4.6 (1M context) * feat(cli): show parent_id and full IDs in comment list The table output now includes a PARENT column and shows full comment IDs (not truncated) so agents can see thread structure and use --parent. Co-Authored-By: Claude Opus 4.6 (1M context) * feat(daemon): instruct agents to always use --output json Agents now see explicit guidance to use --output json for all read commands, ensuring they get structured data with full IDs and parent_id for proper threading. Co-Authored-By: Claude Opus 4.6 (1M context) * feat(daemon): differentiate comment-trigger vs assign-trigger context When triggered by a comment, the agent now gets clear instructions: - Primary goal is to read and respond to the comment - Do NOT change issue status just because you replied - Only change status if explicitly requested This prevents the agent from seeing "In Review" and stopping, since it now understands the task is to reply, not to re-evaluate the issue. Co-Authored-By: Claude Opus 4.6 (1M context) * fix(daemon): split workflow by trigger type in CLAUDE.md/AGENTS.md The Workflow section in the agent's runtime config now shows a comment-reply workflow when triggered by a comment (read comments, find trigger, reply, don't change status) vs the full assignment workflow (set in_progress, do work, set in_review). Previously the agent always saw the assignment workflow, causing it to check the issue status, see "In Review", and stop without reading or replying to the triggering comment. Co-Authored-By: Claude Opus 4.6 (1M context) * refactor(daemon): remove duplicate workflow from issue_context.md Workflow instructions now live only in CLAUDE.md/AGENTS.md (runtime_config.go). issue_context.md keeps just the task data: issue ID, trigger type, and triggering comment ID. Co-Authored-By: Claude Opus 4.6 (1M context) * fix(task): skip duplicate comment on completion for comment-triggered tasks When triggered by a comment, the agent posts its own reply via CLI with --parent. The task completion path was also creating a comment from the agent's stdout output, resulting in duplicates. Now only assignment-triggered tasks auto-post output as a comment. Error messages from FailTask are still posted regardless of trigger type. Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- apps/web/app/(dashboard)/agents/page.tsx | 5 +- server/cmd/multica/cmd_issue.go | 13 +++- server/internal/daemon/daemon.go | 1 + server/internal/daemon/execenv/context.go | 8 ++- server/internal/daemon/execenv/execenv.go | 1 + .../internal/daemon/execenv/runtime_config.go | 62 ++++++++++++------- server/internal/daemon/types.go | 5 +- server/internal/handler/agent.go | 10 +-- server/internal/handler/comment.go | 9 ++- server/internal/service/task.go | 35 +++++++---- .../028_task_trigger_comment.down.sql | 1 + .../028_task_trigger_comment.up.sql | 1 + server/pkg/db/generated/agent.sql.go | 44 ++++++++----- server/pkg/db/generated/models.go | 31 +++++----- server/pkg/db/queries/agent.sql | 4 +- 15 files changed, 150 insertions(+), 80 deletions(-) create mode 100644 server/migrations/028_task_trigger_comment.down.sql create mode 100644 server/migrations/028_task_trigger_comment.up.sql diff --git a/apps/web/app/(dashboard)/agents/page.tsx b/apps/web/app/(dashboard)/agents/page.tsx index 902cbcf4..97f1416c 100644 --- a/apps/web/app/(dashboard)/agents/page.tsx +++ b/apps/web/app/(dashboard)/agents/page.tsx @@ -150,7 +150,10 @@ function CreateAgentDialog({ description: description.trim(), runtime_id: selectedRuntime.id, visibility, - triggers: [{ id: generateId(), type: "on_assign", enabled: true, config: {} }], + triggers: [ + { id: generateId(), type: "on_assign", enabled: true, config: {} }, + { id: generateId(), type: "on_comment", enabled: true, config: {} }, + ], }); onClose(); } catch (err) { diff --git a/server/cmd/multica/cmd_issue.go b/server/cmd/multica/cmd_issue.go index d68a40e3..e7783354 100644 --- a/server/cmd/multica/cmd_issue.go +++ b/server/cmd/multica/cmd_issue.go @@ -146,6 +146,7 @@ func init() { // issue comment add issueCommentAddCmd.Flags().String("content", "", "Comment content (required)") + issueCommentAddCmd.Flags().String("parent", "", "Parent comment ID (reply to a specific comment)") issueCommentAddCmd.Flags().String("output", "json", "Output format: table or json") } @@ -499,7 +500,7 @@ func runIssueCommentList(cmd *cobra.Command, args []string) error { return cli.PrintJSON(os.Stdout, comments) } - headers := []string{"ID", "AUTHOR", "TYPE", "CONTENT", "CREATED"} + headers := []string{"ID", "PARENT", "AUTHOR", "TYPE", "CONTENT", "CREATED"} rows := make([][]string, 0, len(comments)) for _, c := range comments { content := strVal(c, "content") @@ -511,8 +512,13 @@ func runIssueCommentList(cmd *cobra.Command, args []string) error { if len(created) >= 16 { created = created[:16] } + parentID := strVal(c, "parent_id") + if parentID == "" { + parentID = "—" + } rows = append(rows, []string{ - truncateID(strVal(c, "id")), + strVal(c, "id"), + parentID, strVal(c, "author_type") + ":" + truncateID(strVal(c, "author_id")), strVal(c, "type"), content, @@ -538,6 +544,9 @@ func runIssueCommentAdd(cmd *cobra.Command, args []string) error { defer cancel() body := map[string]any{"content": content} + if parentID, _ := cmd.Flags().GetString("parent"); parentID != "" { + body["parent_id"] = parentID + } var result map[string]any if err := client.PostJSON(ctx, "/api/issues/"+args[0]+"/comments", body, &result); err != nil { return fmt.Errorf("add comment: %w", err) diff --git a/server/internal/daemon/daemon.go b/server/internal/daemon/daemon.go index d9b66c58..13f0c5da 100644 --- a/server/internal/daemon/daemon.go +++ b/server/internal/daemon/daemon.go @@ -737,6 +737,7 @@ func (d *Daemon) runTask(ctx context.Context, task Task, provider string, taskLo // via `multica repo checkout `. taskCtx := execenv.TaskContextForEnv{ IssueID: task.IssueID, + TriggerCommentID: task.TriggerCommentID, AgentName: agentName, AgentInstructions: instructions, AgentSkills: convertSkillsForEnv(skills), diff --git a/server/internal/daemon/execenv/context.go b/server/internal/daemon/execenv/context.go index 937c7207..58d5e999 100644 --- a/server/internal/daemon/execenv/context.go +++ b/server/internal/daemon/execenv/context.go @@ -113,8 +113,12 @@ func renderIssueContext(provider string, ctx TaskContextForEnv) string { b.WriteString("# Task Assignment\n\n") fmt.Fprintf(&b, "**Issue ID:** %s\n\n", ctx.IssueID) - b.WriteString("Run `multica issue get " + ctx.IssueID + " --output json` for full issue details and description.\n") - b.WriteString("Run `multica issue comment list " + ctx.IssueID + "` for discussion history.\n\n") + if ctx.TriggerCommentID != "" { + b.WriteString("**Trigger:** Comment Reply\n") + b.WriteString("**Triggering comment ID:** `" + ctx.TriggerCommentID + "`\n\n") + } else { + b.WriteString("**Trigger:** New Assignment\n\n") + } if len(ctx.AgentSkills) > 0 { b.WriteString("## Agent Skills\n\n") diff --git a/server/internal/daemon/execenv/execenv.go b/server/internal/daemon/execenv/execenv.go index 265bfa29..9460be1c 100644 --- a/server/internal/daemon/execenv/execenv.go +++ b/server/internal/daemon/execenv/execenv.go @@ -29,6 +29,7 @@ type PrepareParams struct { // TaskContextForEnv is the subset of task context used for writing context files. type TaskContextForEnv struct { IssueID string + TriggerCommentID string // comment that triggered this task (empty for on_assign) AgentName string AgentInstructions string // agent identity/persona instructions, injected into CLAUDE.md AgentSkills []SkillContextForEnv diff --git a/server/internal/daemon/execenv/runtime_config.go b/server/internal/daemon/execenv/runtime_config.go index aad63fe0..df7a51ef 100644 --- a/server/internal/daemon/execenv/runtime_config.go +++ b/server/internal/daemon/execenv/runtime_config.go @@ -42,15 +42,16 @@ func buildMetaSkillContent(provider string, ctx TaskContextForEnv) string { } b.WriteString("## Available Commands\n\n") + b.WriteString("**Always use `--output json` for all read commands** to get structured data with full IDs.\n\n") b.WriteString("### Read\n") - b.WriteString("- `multica issue get ` — Get full issue details (title, description, status, priority, assignee)\n") - b.WriteString("- `multica issue list [--status X] [--priority X] [--assignee X]` — List issues in workspace\n") - b.WriteString("- `multica issue comment list ` — List all comments on an issue\n") - b.WriteString("- `multica workspace get` — Get workspace details and context\n") - b.WriteString("- `multica agent list` — List agents in workspace\n\n") + b.WriteString("- `multica issue get --output json` — Get full issue details (title, description, status, priority, assignee)\n") + b.WriteString("- `multica issue list [--status X] [--priority X] [--assignee X] --output json` — List issues in workspace\n") + b.WriteString("- `multica issue comment list --output json` — List all comments on an issue (includes id, parent_id for threading)\n") + b.WriteString("- `multica workspace get --output json` — Get workspace details and context\n") + b.WriteString("- `multica agent list --output json` — List agents in workspace\n\n") b.WriteString("### Write\n") - b.WriteString("- `multica issue comment add --content \"...\"` — Post a comment to an issue\n") + b.WriteString("- `multica issue comment add --content \"...\" [--parent ]` — Post a comment (use --parent to reply to a specific comment)\n") b.WriteString("- `multica issue status ` — Update issue status (todo, in_progress, in_review, done, blocked)\n") b.WriteString("- `multica issue update [--title X] [--description X] [--priority X]` — Update issue fields\n\n") @@ -71,26 +72,39 @@ func buildMetaSkillContent(provider string, ctx TaskContextForEnv) string { b.WriteString("\nThe checkout command creates a git worktree with a dedicated branch. You can check out one or more repos as needed.\n\n") } - b.WriteString("### Workflow\n") - b.WriteString("You are responsible for managing the issue status throughout your work.\n\n") - fmt.Fprintf(&b, "1. Run `multica issue get %s --output json` to understand your task\n", ctx.IssueID) - fmt.Fprintf(&b, "2. Run `multica issue status %s in_progress`\n", ctx.IssueID) - b.WriteString("3. Read comments for additional context or human instructions\n") - b.WriteString("4. If the task requires code changes:\n") - if len(ctx.Repos) > 0 { - b.WriteString(" a. Run `multica repo checkout ` to check out the appropriate repository\n") - b.WriteString(" b. `cd` into the checked-out directory\n") - b.WriteString(" c. Implement the changes and commit\n") + b.WriteString("### Workflow\n\n") + + if ctx.TriggerCommentID != "" { + // Comment-triggered: focus on reading and replying + b.WriteString("**This task was triggered by a comment.** Your primary job is to respond.\n\n") + fmt.Fprintf(&b, "1. Run `multica issue get %s --output json` to understand the issue context\n", ctx.IssueID) + fmt.Fprintf(&b, "2. Run `multica issue comment list %s --output json` to read the conversation\n", ctx.IssueID) + fmt.Fprintf(&b, "3. Find the triggering comment (ID: `%s`) and understand what is being asked\n", ctx.TriggerCommentID) + fmt.Fprintf(&b, "4. Reply: `multica issue comment add %s --parent %s --content \"...\"`\n", ctx.IssueID, ctx.TriggerCommentID) + b.WriteString("5. If the comment requests code changes or further work, do the work first, then reply with your results\n") + b.WriteString("6. Do NOT change the issue status unless the comment explicitly asks for it\n\n") } else { - b.WriteString(" a. Create a new branch\n") - b.WriteString(" b. Implement the changes and commit\n") + // Assignment-triggered: full workflow + b.WriteString("You are responsible for managing the issue status throughout your work.\n\n") + fmt.Fprintf(&b, "1. Run `multica issue get %s --output json` to understand your task\n", ctx.IssueID) + fmt.Fprintf(&b, "2. Run `multica issue status %s in_progress`\n", ctx.IssueID) + b.WriteString("3. Read comments for additional context or human instructions\n") + b.WriteString("4. If the task requires code changes:\n") + if len(ctx.Repos) > 0 { + b.WriteString(" a. Run `multica repo checkout ` to check out the appropriate repository\n") + b.WriteString(" b. `cd` into the checked-out directory\n") + b.WriteString(" c. Implement the changes and commit\n") + } else { + b.WriteString(" a. Create a new branch\n") + b.WriteString(" b. Implement the changes and commit\n") + } + b.WriteString(" c. Push the branch to the remote\n") + b.WriteString(" d. Create a pull request (decide the target branch based on the repo's conventions)\n") + fmt.Fprintf(&b, " e. Post the PR link as a comment: `multica issue comment add %s --content \"PR: \"`\n", ctx.IssueID) + b.WriteString("5. If the task does not require code (e.g. research, documentation), post your findings as a comment\n") + fmt.Fprintf(&b, "6. Run `multica issue status %s in_review`\n", ctx.IssueID) + fmt.Fprintf(&b, "7. If blocked, run `multica issue status %s blocked` and post a comment explaining why\n\n", ctx.IssueID) } - b.WriteString(" c. Push the branch to the remote\n") - b.WriteString(" d. Create a pull request (decide the target branch based on the repo's conventions)\n") - fmt.Fprintf(&b, " e. Post the PR link as a comment: `multica issue comment add %s --content \"PR: \"`\n", ctx.IssueID) - b.WriteString("5. If the task does not require code (e.g. research, documentation), post your findings as a comment\n") - fmt.Fprintf(&b, "6. Run `multica issue status %s in_review`\n", ctx.IssueID) - fmt.Fprintf(&b, "7. If blocked, run `multica issue status %s blocked` and post a comment explaining why\n\n", ctx.IssueID) if len(ctx.AgentSkills) > 0 { b.WriteString("## Skills\n\n") diff --git a/server/internal/daemon/types.go b/server/internal/daemon/types.go index 39f8cee5..accafc3f 100644 --- a/server/internal/daemon/types.go +++ b/server/internal/daemon/types.go @@ -30,8 +30,9 @@ type Task struct { WorkspaceID string `json:"workspace_id"` Agent *AgentData `json:"agent,omitempty"` Repos []RepoData `json:"repos,omitempty"` - PriorSessionID string `json:"prior_session_id,omitempty"` // Claude session ID from a previous task on this issue - PriorWorkDir string `json:"prior_work_dir,omitempty"` // work_dir from a previous task on this issue + PriorSessionID string `json:"prior_session_id,omitempty"` // Claude session ID from a previous task on this issue + PriorWorkDir string `json:"prior_work_dir,omitempty"` // work_dir from a previous task on this issue + TriggerCommentID string `json:"trigger_comment_id,omitempty"` // comment that triggered this task } // AgentData holds agent details returned by the claim endpoint. diff --git a/server/internal/handler/agent.go b/server/internal/handler/agent.go index 9ad40468..cc2572cb 100644 --- a/server/internal/handler/agent.go +++ b/server/internal/handler/agent.go @@ -104,8 +104,9 @@ type AgentTaskResponse struct { Agent *TaskAgentData `json:"agent,omitempty"` Repos []RepoData `json:"repos,omitempty"` CreatedAt string `json:"created_at"` - PriorSessionID string `json:"prior_session_id,omitempty"` // session ID from a previous task on same issue - PriorWorkDir string `json:"prior_work_dir,omitempty"` // work_dir from a previous task on same issue + PriorSessionID string `json:"prior_session_id,omitempty"` // session ID from a previous task on same issue + PriorWorkDir string `json:"prior_work_dir,omitempty"` // work_dir from a previous task on same issue + TriggerCommentID *string `json:"trigger_comment_id,omitempty"` // comment that triggered this task } // TaskAgentData holds agent info included in claim responses so the daemon @@ -133,8 +134,9 @@ func taskToResponse(t db.AgentTaskQueue) AgentTaskResponse { StartedAt: timestampToPtr(t.StartedAt), CompletedAt: timestampToPtr(t.CompletedAt), Result: result, - Error: textToPtr(t.Error), - CreatedAt: timestampToString(t.CreatedAt), + Error: textToPtr(t.Error), + CreatedAt: timestampToString(t.CreatedAt), + TriggerCommentID: uuidToPtr(t.TriggerCommentID), } } diff --git a/server/internal/handler/comment.go b/server/internal/handler/comment.go index e8869072..a1b2c38c 100644 --- a/server/internal/handler/comment.go +++ b/server/internal/handler/comment.go @@ -146,7 +146,14 @@ func (h *Handler) CreateComment(w http.ResponseWriter, r *http.Request) { // If the issue is assigned to an agent with on_comment trigger, enqueue a new task. // Skip when the comment comes from the assigned agent itself to avoid loops. if authorType == "member" && h.shouldEnqueueOnComment(r.Context(), issue) { - if _, err := h.TaskService.EnqueueTaskForIssue(r.Context(), issue); err != nil { + // Resolve thread root: if the comment is a reply, agent should reply + // to the thread root (matching frontend behavior where all replies + // in a thread share the same top-level parent). + replyTo := comment.ID + if comment.ParentID.Valid { + replyTo = comment.ParentID + } + if _, err := h.TaskService.EnqueueTaskForIssue(r.Context(), issue, replyTo); err != nil { slog.Warn("enqueue agent task on comment failed", "issue_id", issueID, "error", err) } } diff --git a/server/internal/service/task.go b/server/internal/service/task.go index 581bd918..b2574aba 100644 --- a/server/internal/service/task.go +++ b/server/internal/service/task.go @@ -32,7 +32,7 @@ func NewTaskService(q *db.Queries, hub *realtime.Hub, bus *events.Bus) *TaskServ // EnqueueTaskForIssue creates a queued task for an agent-assigned issue. // No context snapshot is stored — the agent fetches all data it needs at // runtime via the multica CLI. -func (s *TaskService) EnqueueTaskForIssue(ctx context.Context, issue db.Issue) (db.AgentTaskQueue, error) { +func (s *TaskService) EnqueueTaskForIssue(ctx context.Context, issue db.Issue, triggerCommentID ...pgtype.UUID) (db.AgentTaskQueue, error) { if !issue.AssigneeID.Valid { slog.Error("task enqueue failed", "issue_id", util.UUIDToString(issue.ID), "error", "issue has no assignee") return db.AgentTaskQueue{}, fmt.Errorf("issue has no assignee") @@ -48,11 +48,17 @@ func (s *TaskService) EnqueueTaskForIssue(ctx context.Context, issue db.Issue) ( return db.AgentTaskQueue{}, fmt.Errorf("agent has no runtime") } + var commentID pgtype.UUID + if len(triggerCommentID) > 0 { + commentID = triggerCommentID[0] + } + task, err := s.Queries.CreateAgentTask(ctx, db.CreateAgentTaskParams{ - AgentID: issue.AssigneeID, - RuntimeID: agent.RuntimeID, - IssueID: issue.ID, - Priority: priorityToInt(issue.Priority), + AgentID: issue.AssigneeID, + RuntimeID: agent.RuntimeID, + IssueID: issue.ID, + Priority: priorityToInt(issue.Priority), + TriggerCommentID: commentID, }) if err != nil { slog.Error("task enqueue failed", "issue_id", util.UUIDToString(issue.ID), "error", err) @@ -185,10 +191,15 @@ func (s *TaskService) CompleteTask(ctx context.Context, taskID pgtype.UUID, resu slog.Info("task completed", "task_id", util.UUIDToString(task.ID), "issue_id", util.UUIDToString(task.IssueID)) - var payload protocol.TaskCompletedPayload - if err := json.Unmarshal(result, &payload); err == nil { - if payload.Output != "" { - s.createAgentComment(ctx, task.IssueID, task.AgentID, redact.Text(payload.Output), "comment") + // Post agent output as a comment, but only for assignment-triggered tasks. + // Comment-triggered tasks: the agent replies via CLI with --parent, so + // posting here would create a duplicate. + if !task.TriggerCommentID.Valid { + var payload protocol.TaskCompletedPayload + if err := json.Unmarshal(result, &payload); err == nil { + if payload.Output != "" { + s.createAgentComment(ctx, task.IssueID, task.AgentID, redact.Text(payload.Output), "comment", task.TriggerCommentID) + } } } @@ -228,7 +239,7 @@ func (s *TaskService) FailTask(ctx context.Context, taskID pgtype.UUID, errMsg s slog.Warn("task failed", "task_id", util.UUIDToString(task.ID), "issue_id", util.UUIDToString(task.IssueID), "error", errMsg) if errMsg != "" { - s.createAgentComment(ctx, task.IssueID, task.AgentID, redact.Text(errMsg), "system") + s.createAgentComment(ctx, task.IssueID, task.AgentID, redact.Text(errMsg), "system", task.TriggerCommentID) } // Reconcile agent status s.ReconcileAgentStatus(ctx, task.AgentID) @@ -401,7 +412,7 @@ func (s *TaskService) getIssuePrefix(workspaceID pgtype.UUID) string { return ws.IssuePrefix } -func (s *TaskService) createAgentComment(ctx context.Context, issueID, agentID pgtype.UUID, content, commentType string) { +func (s *TaskService) createAgentComment(ctx context.Context, issueID, agentID pgtype.UUID, content, commentType string, parentID pgtype.UUID) { if content == "" { return } @@ -411,6 +422,7 @@ func (s *TaskService) createAgentComment(ctx context.Context, issueID, agentID p AuthorID: agentID, Content: content, Type: commentType, + ParentID: parentID, }) if err != nil { return @@ -433,6 +445,7 @@ func (s *TaskService) createAgentComment(ctx context.Context, issueID, agentID p "author_id": util.UUIDToString(comment.AuthorID), "content": comment.Content, "type": comment.Type, + "parent_id": util.UUIDToPtr(comment.ParentID), "created_at": comment.CreatedAt.Time.Format("2006-01-02T15:04:05Z"), }, "issue_title": issue.Title, diff --git a/server/migrations/028_task_trigger_comment.down.sql b/server/migrations/028_task_trigger_comment.down.sql new file mode 100644 index 00000000..1ba48917 --- /dev/null +++ b/server/migrations/028_task_trigger_comment.down.sql @@ -0,0 +1 @@ +ALTER TABLE agent_task_queue DROP COLUMN trigger_comment_id; diff --git a/server/migrations/028_task_trigger_comment.up.sql b/server/migrations/028_task_trigger_comment.up.sql new file mode 100644 index 00000000..e0ebd685 --- /dev/null +++ b/server/migrations/028_task_trigger_comment.up.sql @@ -0,0 +1 @@ +ALTER TABLE agent_task_queue ADD COLUMN trigger_comment_id UUID REFERENCES comment(id) ON DELETE SET NULL; diff --git a/server/pkg/db/generated/agent.sql.go b/server/pkg/db/generated/agent.sql.go index 1de2793f..f345e656 100644 --- a/server/pkg/db/generated/agent.sql.go +++ b/server/pkg/db/generated/agent.sql.go @@ -32,7 +32,7 @@ WHERE id = ( LIMIT 1 FOR UPDATE SKIP LOCKED ) -RETURNING id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir +RETURNING id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id ` func (q *Queries) ClaimAgentTask(ctx context.Context, agentID pgtype.UUID) (AgentTaskQueue, error) { @@ -54,6 +54,7 @@ func (q *Queries) ClaimAgentTask(ctx context.Context, agentID pgtype.UUID) (Agen &i.RuntimeID, &i.SessionID, &i.WorkDir, + &i.TriggerCommentID, ) return i, err } @@ -62,7 +63,7 @@ const completeAgentTask = `-- name: CompleteAgentTask :one UPDATE agent_task_queue SET status = 'completed', completed_at = now(), result = $2, session_id = $3, work_dir = $4 WHERE id = $1 AND status = 'running' -RETURNING id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir +RETURNING id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id ` type CompleteAgentTaskParams struct { @@ -96,6 +97,7 @@ func (q *Queries) CompleteAgentTask(ctx context.Context, arg CompleteAgentTaskPa &i.RuntimeID, &i.SessionID, &i.WorkDir, + &i.TriggerCommentID, ) return i, err } @@ -177,16 +179,17 @@ func (q *Queries) CreateAgent(ctx context.Context, arg CreateAgentParams) (Agent } const createAgentTask = `-- name: CreateAgentTask :one -INSERT INTO agent_task_queue (agent_id, runtime_id, issue_id, status, priority) -VALUES ($1, $2, $3, 'queued', $4) -RETURNING id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir +INSERT INTO agent_task_queue (agent_id, runtime_id, issue_id, status, priority, trigger_comment_id) +VALUES ($1, $2, $3, 'queued', $4, $5) +RETURNING id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id ` type CreateAgentTaskParams struct { - AgentID pgtype.UUID `json:"agent_id"` - RuntimeID pgtype.UUID `json:"runtime_id"` - IssueID pgtype.UUID `json:"issue_id"` - Priority int32 `json:"priority"` + AgentID pgtype.UUID `json:"agent_id"` + RuntimeID pgtype.UUID `json:"runtime_id"` + IssueID pgtype.UUID `json:"issue_id"` + Priority int32 `json:"priority"` + TriggerCommentID pgtype.UUID `json:"trigger_comment_id"` } func (q *Queries) CreateAgentTask(ctx context.Context, arg CreateAgentTaskParams) (AgentTaskQueue, error) { @@ -195,6 +198,7 @@ func (q *Queries) CreateAgentTask(ctx context.Context, arg CreateAgentTaskParams arg.RuntimeID, arg.IssueID, arg.Priority, + arg.TriggerCommentID, ) var i AgentTaskQueue err := row.Scan( @@ -213,6 +217,7 @@ func (q *Queries) CreateAgentTask(ctx context.Context, arg CreateAgentTaskParams &i.RuntimeID, &i.SessionID, &i.WorkDir, + &i.TriggerCommentID, ) return i, err } @@ -230,7 +235,7 @@ const failAgentTask = `-- name: FailAgentTask :one UPDATE agent_task_queue SET status = 'failed', completed_at = now(), error = $2 WHERE id = $1 AND status IN ('dispatched', 'running') -RETURNING id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir +RETURNING id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id ` type FailAgentTaskParams struct { @@ -257,6 +262,7 @@ func (q *Queries) FailAgentTask(ctx context.Context, arg FailAgentTaskParams) (A &i.RuntimeID, &i.SessionID, &i.WorkDir, + &i.TriggerCommentID, ) return i, err } @@ -369,7 +375,7 @@ func (q *Queries) GetAgentInWorkspace(ctx context.Context, arg GetAgentInWorkspa } const getAgentTask = `-- name: GetAgentTask :one -SELECT id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir FROM agent_task_queue +SELECT id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id FROM agent_task_queue WHERE id = $1 ` @@ -392,6 +398,7 @@ func (q *Queries) GetAgentTask(ctx context.Context, id pgtype.UUID) (AgentTaskQu &i.RuntimeID, &i.SessionID, &i.WorkDir, + &i.TriggerCommentID, ) return i, err } @@ -452,7 +459,7 @@ func (q *Queries) HasPendingTaskForIssue(ctx context.Context, issueID pgtype.UUI } const listActiveTasksByIssue = `-- name: ListActiveTasksByIssue :many -SELECT id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir FROM agent_task_queue +SELECT id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id FROM agent_task_queue WHERE issue_id = $1 AND status IN ('dispatched', 'running') ORDER BY created_at DESC ` @@ -482,6 +489,7 @@ func (q *Queries) ListActiveTasksByIssue(ctx context.Context, issueID pgtype.UUI &i.RuntimeID, &i.SessionID, &i.WorkDir, + &i.TriggerCommentID, ); err != nil { return nil, err } @@ -494,7 +502,7 @@ func (q *Queries) ListActiveTasksByIssue(ctx context.Context, issueID pgtype.UUI } const listAgentTasks = `-- name: ListAgentTasks :many -SELECT id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir FROM agent_task_queue +SELECT id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id FROM agent_task_queue WHERE agent_id = $1 ORDER BY created_at DESC ` @@ -524,6 +532,7 @@ func (q *Queries) ListAgentTasks(ctx context.Context, agentID pgtype.UUID) ([]Ag &i.RuntimeID, &i.SessionID, &i.WorkDir, + &i.TriggerCommentID, ); err != nil { return nil, err } @@ -580,7 +589,7 @@ func (q *Queries) ListAgents(ctx context.Context, workspaceID pgtype.UUID) ([]Ag } const listPendingTasksByRuntime = `-- name: ListPendingTasksByRuntime :many -SELECT id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir FROM agent_task_queue +SELECT id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id FROM agent_task_queue WHERE runtime_id = $1 AND status IN ('queued', 'dispatched') ORDER BY priority DESC, created_at ASC ` @@ -610,6 +619,7 @@ func (q *Queries) ListPendingTasksByRuntime(ctx context.Context, runtimeID pgtyp &i.RuntimeID, &i.SessionID, &i.WorkDir, + &i.TriggerCommentID, ); err != nil { return nil, err } @@ -622,7 +632,7 @@ func (q *Queries) ListPendingTasksByRuntime(ctx context.Context, runtimeID pgtyp } const listTasksByIssue = `-- name: ListTasksByIssue :many -SELECT id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir FROM agent_task_queue +SELECT id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id FROM agent_task_queue WHERE issue_id = $1 ORDER BY created_at DESC ` @@ -652,6 +662,7 @@ func (q *Queries) ListTasksByIssue(ctx context.Context, issueID pgtype.UUID) ([] &i.RuntimeID, &i.SessionID, &i.WorkDir, + &i.TriggerCommentID, ); err != nil { return nil, err } @@ -667,7 +678,7 @@ const startAgentTask = `-- name: StartAgentTask :one UPDATE agent_task_queue SET status = 'running', started_at = now() WHERE id = $1 AND status = 'dispatched' -RETURNING id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir +RETURNING id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id ` func (q *Queries) StartAgentTask(ctx context.Context, id pgtype.UUID) (AgentTaskQueue, error) { @@ -689,6 +700,7 @@ func (q *Queries) StartAgentTask(ctx context.Context, id pgtype.UUID) (AgentTask &i.RuntimeID, &i.SessionID, &i.WorkDir, + &i.TriggerCommentID, ) return i, err } diff --git a/server/pkg/db/generated/models.go b/server/pkg/db/generated/models.go index ecd575be..9547212e 100644 --- a/server/pkg/db/generated/models.go +++ b/server/pkg/db/generated/models.go @@ -61,21 +61,22 @@ type AgentSkill struct { } type AgentTaskQueue struct { - ID pgtype.UUID `json:"id"` - AgentID pgtype.UUID `json:"agent_id"` - IssueID pgtype.UUID `json:"issue_id"` - Status string `json:"status"` - Priority int32 `json:"priority"` - DispatchedAt pgtype.Timestamptz `json:"dispatched_at"` - StartedAt pgtype.Timestamptz `json:"started_at"` - CompletedAt pgtype.Timestamptz `json:"completed_at"` - Result []byte `json:"result"` - Error pgtype.Text `json:"error"` - CreatedAt pgtype.Timestamptz `json:"created_at"` - Context []byte `json:"context"` - RuntimeID pgtype.UUID `json:"runtime_id"` - SessionID pgtype.Text `json:"session_id"` - WorkDir pgtype.Text `json:"work_dir"` + ID pgtype.UUID `json:"id"` + AgentID pgtype.UUID `json:"agent_id"` + IssueID pgtype.UUID `json:"issue_id"` + Status string `json:"status"` + Priority int32 `json:"priority"` + DispatchedAt pgtype.Timestamptz `json:"dispatched_at"` + StartedAt pgtype.Timestamptz `json:"started_at"` + CompletedAt pgtype.Timestamptz `json:"completed_at"` + Result []byte `json:"result"` + Error pgtype.Text `json:"error"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + Context []byte `json:"context"` + RuntimeID pgtype.UUID `json:"runtime_id"` + SessionID pgtype.Text `json:"session_id"` + WorkDir pgtype.Text `json:"work_dir"` + TriggerCommentID pgtype.UUID `json:"trigger_comment_id"` } type Comment struct { diff --git a/server/pkg/db/queries/agent.sql b/server/pkg/db/queries/agent.sql index 4540a7e9..83dd1a1b 100644 --- a/server/pkg/db/queries/agent.sql +++ b/server/pkg/db/queries/agent.sql @@ -46,8 +46,8 @@ WHERE agent_id = $1 ORDER BY created_at DESC; -- name: CreateAgentTask :one -INSERT INTO agent_task_queue (agent_id, runtime_id, issue_id, status, priority) -VALUES ($1, $2, $3, 'queued', $4) +INSERT INTO agent_task_queue (agent_id, runtime_id, issue_id, status, priority, trigger_comment_id) +VALUES ($1, $2, $3, 'queued', $4, sqlc.narg(trigger_comment_id)) RETURNING *; -- name: CancelAgentTasksByIssue :exec