fix(handler): attribute agent CLI actions to agent identity

When agents use the multica CLI during task execution, their comments,
issue updates, and issue creations were attributed to the daemon's user
(via JWT) instead of the agent. Pass MULTICA_AGENT_ID env var from the
daemon, send X-Agent-ID header from the CLI client, and use it in
handlers to set the correct author/actor identity.
This commit is contained in:
Jiayuan 2026-03-30 02:41:51 +08:00
parent 663dec52b8
commit a4c8bbb03c
5 changed files with 52 additions and 10 deletions

View file

@ -101,10 +101,22 @@ func (h *Handler) CreateComment(w http.ResponseWriter, r *http.Request) {
}
}
// Determine author identity: agent (via X-Agent-ID header) or member.
authorType := "member"
authorID := userID
if agentID := r.Header.Get("X-Agent-ID"); agentID != "" {
// Validate the agent exists in this workspace.
agent, err := h.Queries.GetAgent(r.Context(), parseUUID(agentID))
if err == nil && uuidToString(agent.WorkspaceID) == uuidToString(issue.WorkspaceID) {
authorType = "agent"
authorID = agentID
}
}
comment, err := h.Queries.CreateComment(r.Context(), db.CreateCommentParams{
IssueID: issue.ID,
AuthorType: "member",
AuthorID: parseUUID(userID),
AuthorType: authorType,
AuthorID: parseUUID(authorID),
Content: req.Content,
Type: req.Type,
ParentID: parentID,
@ -117,7 +129,7 @@ func (h *Handler) CreateComment(w http.ResponseWriter, r *http.Request) {
resp := commentToResponse(comment)
slog.Info("comment created", append(logger.RequestAttrs(r), "comment_id", uuidToString(comment.ID), "issue_id", issueID)...)
h.publish(protocol.EventCommentCreated, uuidToString(issue.WorkspaceID), "member", userID, map[string]any{
h.publish(protocol.EventCommentCreated, uuidToString(issue.WorkspaceID), authorType, authorID, map[string]any{
"comment": resp,
"issue_title": issue.Title,
"issue_assignee_type": textToPtr(issue.AssigneeType),
@ -126,8 +138,8 @@ 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.
// The agent will resume its prior session and see this comment.
if h.shouldEnqueueOnComment(r.Context(), issue) {
// 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 {
slog.Warn("enqueue agent task on comment failed", "issue_id", issueID, "error", err)
}

View file

@ -220,6 +220,16 @@ func (h *Handler) CreateIssue(w http.ResponseWriter, r *http.Request) {
return
}
// Determine creator identity: agent (via X-Agent-ID header) or member.
creatorType := "member"
actualCreatorID := creatorID
if agentID := r.Header.Get("X-Agent-ID"); agentID != "" {
if agent, err := h.Queries.GetAgent(r.Context(), parseUUID(agentID)); err == nil && uuidToString(agent.WorkspaceID) == workspaceID {
creatorType = "agent"
actualCreatorID = agentID
}
}
issue, err := qtx.CreateIssue(r.Context(), db.CreateIssueParams{
WorkspaceID: parseUUID(workspaceID),
Title: req.Title,
@ -228,8 +238,8 @@ func (h *Handler) CreateIssue(w http.ResponseWriter, r *http.Request) {
Priority: priority,
AssigneeType: assigneeType,
AssigneeID: assigneeID,
CreatorType: "member",
CreatorID: parseUUID(creatorID),
CreatorType: creatorType,
CreatorID: parseUUID(actualCreatorID),
ParentIssueID: parentIssueID,
Position: 0,
DueDate: dueDate,
@ -249,7 +259,7 @@ func (h *Handler) CreateIssue(w http.ResponseWriter, r *http.Request) {
prefix := h.getIssuePrefix(r.Context(), issue.WorkspaceID)
resp := issueToResponse(issue, prefix)
slog.Info("issue created", append(logger.RequestAttrs(r), "issue_id", uuidToString(issue.ID), "title", issue.Title, "status", issue.Status, "workspace_id", workspaceID)...)
h.publish(protocol.EventIssueCreated, workspaceID, "member", creatorID, map[string]any{"issue": resp})
h.publish(protocol.EventIssueCreated, workspaceID, creatorType, actualCreatorID, map[string]any{"issue": resp})
// Only ready issues in todo are enqueued for agents.
if issue.AssigneeType.Valid && issue.AssigneeID.Valid {
@ -371,7 +381,17 @@ func (h *Handler) UpdateIssue(w http.ResponseWriter, r *http.Request) {
dueDateChanged := prevDueDate != resp.DueDate && (prevDueDate == nil) != (resp.DueDate == nil) ||
(prevDueDate != nil && resp.DueDate != nil && *prevDueDate != *resp.DueDate)
h.publish(protocol.EventIssueUpdated, workspaceID, "member", userID, map[string]any{
// Determine actor identity: agent (via X-Agent-ID header) or member.
actorType := "member"
actorID := userID
if agentID := r.Header.Get("X-Agent-ID"); agentID != "" {
if agent, err := h.Queries.GetAgent(r.Context(), parseUUID(agentID)); err == nil && uuidToString(agent.WorkspaceID) == workspaceID {
actorType = "agent"
actorID = agentID
}
}
h.publish(protocol.EventIssueUpdated, workspaceID, actorType, actorID, map[string]any{
"issue": resp,
"assignee_changed": assigneeChanged,
"status_changed": statusChanged,