fix(coderd): gate AI task notifications on agent ready state (#20690)

Relates to https://github.com/coder/internal/issues/1098

Currently AgentAPI waits for only 2 seconds worth of identical terminal
screen snapshots before deciding a task has entered a "stable" state. We
interpret this as becoming "idle", resulting in a notification being
triggered. This behavior is not ideal and is ultimately the root cause
of our spammy notifications.

Unfortunately, until we move AgentAPI to either use the Claude Code SDK
(or ACP wrapper around it), we are unable to easily fix the root cause.

This PR instead waits until the agent is ready before it will send state
change notifications. This will at least resolve _some_ of the
complaints about task state notifications being too spammy.

---

🤖 PR was written by Claude Sonnet 4.5 using [Coder
Mux](https://github.com/coder/cmux) and reviewed by a human 👩
This commit is contained in:
Danielle Maywood
2025-11-10 16:00:13 +00:00
committed by GitHub
parent 616db320d7
commit f2a1a7e8c3
2 changed files with 83 additions and 2 deletions
+16 -2
View File
@@ -428,7 +428,7 @@ func (api *API) patchWorkspaceAgentAppStatus(rw http.ResponseWriter, r *http.Req
})
// Notify on state change to Working/Idle for AI tasks
api.enqueueAITaskStateNotification(ctx, app.ID, latestAppStatus, req.State, workspace)
api.enqueueAITaskStateNotification(ctx, app.ID, latestAppStatus, req.State, workspace, workspaceAgent)
httpapi.Write(ctx, rw, http.StatusOK, nil)
}
@@ -437,13 +437,15 @@ func (api *API) patchWorkspaceAgentAppStatus(rw http.ResponseWriter, r *http.Req
// 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.
// - the new state equals the latest persisted state,
// - the workspace agent is not ready (still starting up).
func (api *API) enqueueAITaskStateNotification(
ctx context.Context,
appID uuid.UUID,
latestAppStatus []database.WorkspaceAppStatus,
newAppStatus codersdk.WorkspaceAppStatusState,
workspace database.Workspace,
agent database.WorkspaceAgent,
) {
// Select notification template based on the new state
var notificationTemplate uuid.UUID
@@ -466,6 +468,18 @@ func (api *API) enqueueAITaskStateNotification(
return
}
// Only send notifications when the agent is ready. We want to skip
// any state transitions that occur whilst the workspace is starting
// up as it doesn't make sense to receive them.
if agent.LifecycleState != database.WorkspaceAgentLifecycleStateReady {
api.Logger.Debug(ctx, "skipping AI task notification because agent is not ready",
slog.F("agent_id", agent.ID),
slog.F("lifecycle_state", agent.LifecycleState),
slog.F("new_app_status", newAppStatus),
)
return
}
task, err := api.Database.GetTaskByID(ctx, workspace.TaskID.UUID)
if err != nil {
api.Logger.Warn(ctx, "failed to get task", slog.Error(err))