feat(agents): reply as thread instead of top-level comment (#205)

* 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) <noreply@anthropic.com>

* 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 <comment-id>.

- 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) <noreply@anthropic.com>

* 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) <noreply@anthropic.com>

* 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) <noreply@anthropic.com>

* 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) <noreply@anthropic.com>

* 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) <noreply@anthropic.com>

* 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) <noreply@anthropic.com>

* 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) <noreply@anthropic.com>

* 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) <noreply@anthropic.com>

* 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) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
LinYushen 2026-03-31 13:48:39 +08:00 committed by GitHub
parent 5517136d73
commit 961de18c97
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 150 additions and 80 deletions

View file

@ -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) {

View file

@ -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)

View file

@ -737,6 +737,7 @@ func (d *Daemon) runTask(ctx context.Context, task Task, provider string, taskLo
// via `multica repo checkout <url>`.
taskCtx := execenv.TaskContextForEnv{
IssueID: task.IssueID,
TriggerCommentID: task.TriggerCommentID,
AgentName: agentName,
AgentInstructions: instructions,
AgentSkills: convertSkillsForEnv(skills),

View file

@ -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")

View file

@ -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

View file

@ -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 <id>` — 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 <issue-id>` — 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 <id> --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 <issue-id> --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 <issue-id> --content \"...\"` — Post a comment to an issue\n")
b.WriteString("- `multica issue comment add <issue-id> --content \"...\" [--parent <comment-id>]` — Post a comment (use --parent to reply to a specific comment)\n")
b.WriteString("- `multica issue status <id> <status>` — Update issue status (todo, in_progress, in_review, done, blocked)\n")
b.WriteString("- `multica issue update <id> [--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 <url>` 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 <url>` 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: <url>\"`\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: <url>\"`\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")

View file

@ -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.

View file

@ -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),
}
}

View file

@ -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)
}
}

View file

@ -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,

View file

@ -0,0 +1 @@
ALTER TABLE agent_task_queue DROP COLUMN trigger_comment_id;

View file

@ -0,0 +1 @@
ALTER TABLE agent_task_queue ADD COLUMN trigger_comment_id UUID REFERENCES comment(id) ON DELETE SET NULL;

View file

@ -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
}

View file

@ -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 {

View file

@ -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