mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add notification for task status (#19965)
## Description Send a notification to the workspace owner when an AI task’s app state becomes `Working` or `Idle`. An AI task is identified by a workspace build with `HasAITask = true` and `AITaskSidebarAppID` matching the agent app’s ID. ## Changes * Add `TemplateTaskWorking` notification template. * Add `TemplateTaskIdle` notification template. * Add `GetLatestWorkspaceAppStatusesByAppID` SQL query to get the workspace app statuses ordered by latest first. * Update `PATCH /workspaceagents/me/app-status` to enqueue: * `TemplateTaskWorking` when state transitions to `working` * `TemplateTaskIdle` when state transitions to `idle` * Notification labels include: * `task`: task initial prompt * `workspace`: workspace name * Notification dedupe: include a minute-bucketed timestamp (UTC truncated to the minute) in the enqueue data to allow identical content to resend within the same day (but not more than once per minute). Closes: https://github.com/coder/coder/issues/19776
This commit is contained in:
@@ -36,6 +36,7 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/httpmw"
|
||||
"github.com/coder/coder/v2/coderd/httpmw/loggermw"
|
||||
"github.com/coder/coder/v2/coderd/jwtutils"
|
||||
"github.com/coder/coder/v2/coderd/notifications"
|
||||
"github.com/coder/coder/v2/coderd/prebuilds"
|
||||
"github.com/coder/coder/v2/coderd/rbac"
|
||||
"github.com/coder/coder/v2/coderd/rbac/policy"
|
||||
@@ -387,6 +388,17 @@ func (api *API) patchWorkspaceAgentAppStatus(rw http.ResponseWriter, r *http.Req
|
||||
// Treat the message as untrusted input.
|
||||
cleaned := strutil.UISanitize(req.Message)
|
||||
|
||||
// Get the latest statuses for the workspace app to detect no-op updates
|
||||
// nolint:gocritic // This is a system restricted operation.
|
||||
latestAppStatus, err := api.Database.GetLatestWorkspaceAppStatusesByAppID(dbauthz.AsSystemRestricted(ctx), app.ID)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Failed to get latest workspace app statuses.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// nolint:gocritic // This is a system restricted operation.
|
||||
_, err = api.Database.InsertWorkspaceAppStatus(dbauthz.AsSystemRestricted(ctx), database.InsertWorkspaceAppStatusParams{
|
||||
ID: uuid.New(),
|
||||
@@ -415,9 +427,88 @@ func (api *API) patchWorkspaceAgentAppStatus(rw http.ResponseWriter, r *http.Req
|
||||
AgentID: &workspaceAgent.ID,
|
||||
})
|
||||
|
||||
// Notify on state change to Working/Idle for AI tasks
|
||||
api.enqueueAITaskStateNotification(ctx, app.ID, latestAppStatus, req.State, workspace)
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusOK, nil)
|
||||
}
|
||||
|
||||
// enqueueAITaskStateNotification enqueues a notification when an AI task's app
|
||||
// transitions to Working or Idle.
|
||||
// No-op if:
|
||||
// - the workspace agent app isn't configured as an AI task,
|
||||
// - the new state equals the latest persisted state.
|
||||
func (api *API) enqueueAITaskStateNotification(
|
||||
ctx context.Context,
|
||||
appID uuid.UUID,
|
||||
latestAppStatus []database.WorkspaceAppStatus,
|
||||
newAppStatus codersdk.WorkspaceAppStatusState,
|
||||
workspace database.Workspace,
|
||||
) {
|
||||
// Select notification template based on the new state
|
||||
var notificationTemplate uuid.UUID
|
||||
switch newAppStatus {
|
||||
case codersdk.WorkspaceAppStatusStateWorking:
|
||||
notificationTemplate = notifications.TemplateTaskWorking
|
||||
case codersdk.WorkspaceAppStatusStateIdle:
|
||||
notificationTemplate = notifications.TemplateTaskIdle
|
||||
default:
|
||||
// Not a notifiable state, do nothing
|
||||
return
|
||||
}
|
||||
|
||||
workspaceBuild, err := api.Database.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspace.ID)
|
||||
if err != nil {
|
||||
api.Logger.Warn(ctx, "failed to get workspace build", slog.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
// Confirm Workspace Agent App is an AI Task
|
||||
if workspaceBuild.HasAITask.Valid && workspaceBuild.HasAITask.Bool &&
|
||||
workspaceBuild.AITaskSidebarAppID.Valid && workspaceBuild.AITaskSidebarAppID.UUID == appID {
|
||||
// Skip if the latest persisted state equals the new state (no new transition)
|
||||
if len(latestAppStatus) > 0 && latestAppStatus[0].State == database.WorkspaceAppStatusState(newAppStatus) {
|
||||
return
|
||||
}
|
||||
|
||||
// Use the task prompt as the "task" label, fallback to workspace name
|
||||
parameters, err := api.Database.GetWorkspaceBuildParameters(ctx, workspaceBuild.ID)
|
||||
if err != nil {
|
||||
api.Logger.Warn(ctx, "failed to get workspace build parameters", slog.Error(err))
|
||||
return
|
||||
}
|
||||
taskName := workspace.Name
|
||||
for _, param := range parameters {
|
||||
if param.Name == codersdk.AITaskPromptParameterName {
|
||||
taskName = param.Value
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := api.NotificationsEnqueuer.EnqueueWithData(
|
||||
// nolint:gocritic // Need notifier actor to enqueue notifications
|
||||
dbauthz.AsNotifier(ctx),
|
||||
workspace.OwnerID,
|
||||
notificationTemplate,
|
||||
map[string]string{
|
||||
"task": taskName,
|
||||
"workspace": workspace.Name,
|
||||
},
|
||||
map[string]any{
|
||||
// Use a 10-second bucketed timestamp to bypass per-day dedupe,
|
||||
// allowing identical content to resend within the same day
|
||||
// (but not more than once every 10s).
|
||||
"dedupe_bypass_ts": api.Clock.Now().UTC().Truncate(10 * time.Second),
|
||||
},
|
||||
"api-workspace-agent-app-status",
|
||||
// Associate this notification with related entities
|
||||
workspace.ID, workspace.OwnerID, workspace.OrganizationID, appID,
|
||||
); err != nil {
|
||||
api.Logger.Warn(ctx, "failed to notify of task state", slog.Error(err))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// workspaceAgentLogs returns the logs associated with a workspace agent
|
||||
//
|
||||
// @Summary Get logs by workspace agent
|
||||
|
||||
Reference in New Issue
Block a user