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:
Susana Ferreira
2025-09-29 16:44:53 +01:00
committed by GitHub
parent abdea7213a
commit fdb0267e5d
18 changed files with 689 additions and 1 deletions
+39
View File
@@ -18841,6 +18841,45 @@ func (q *sqlQuerier) UpsertWorkspaceAppAuditSession(ctx context.Context, arg Ups
return new_or_stale, err
}
const getLatestWorkspaceAppStatusesByAppID = `-- name: GetLatestWorkspaceAppStatusesByAppID :many
SELECT id, created_at, agent_id, app_id, workspace_id, state, message, uri
FROM workspace_app_statuses
WHERE app_id = $1::uuid
ORDER BY created_at DESC, id DESC
`
func (q *sqlQuerier) GetLatestWorkspaceAppStatusesByAppID(ctx context.Context, appID uuid.UUID) ([]WorkspaceAppStatus, error) {
rows, err := q.db.QueryContext(ctx, getLatestWorkspaceAppStatusesByAppID, appID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []WorkspaceAppStatus
for rows.Next() {
var i WorkspaceAppStatus
if err := rows.Scan(
&i.ID,
&i.CreatedAt,
&i.AgentID,
&i.AppID,
&i.WorkspaceID,
&i.State,
&i.Message,
&i.Uri,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getLatestWorkspaceAppStatusesByWorkspaceIDs = `-- name: GetLatestWorkspaceAppStatusesByWorkspaceIDs :many
SELECT DISTINCT ON (workspace_id)
id, created_at, agent_id, app_id, workspace_id, state, message, uri