From 86bdedb0a90eaf7467bbc9d9958a85cbc2b2d158 Mon Sep 17 00:00:00 2001 From: Hugo Dutka Date: Mon, 15 Jun 2026 17:58:36 +0200 Subject: [PATCH] fix(coderd/x/chatd): dont send web notifs on subagent completion (#26379) The chat refactor mistakenly started sending web push notifications on subagent completion. This PR fixes that. Addresses [CODAGT-624](https://linear.app/codercom/issue/CODAGT-624/subagent-completion-sends-web-push-notifications). --- coderd/x/chatd/generation_preparer.go | 4 +- coderd/x/chatd/turn_summary_internal_test.go | 76 ++++++++++++++++++++ 2 files changed, 77 insertions(+), 3 deletions(-) diff --git a/coderd/x/chatd/generation_preparer.go b/coderd/x/chatd/generation_preparer.go index 215f0b096a..5a8e4fd598 100644 --- a/coderd/x/chatd/generation_preparer.go +++ b/coderd/x/chatd/generation_preparer.go @@ -674,9 +674,7 @@ func (server *Server) afterGenerationOutcome( case runnerActionKindFinishTurn: finalizeCtx := context.WithoutCancel(ctx) runResult := server.deriveFinalTurnRunResult(finalizeCtx, chat, logger) - statusLabel := server.generateFinalTurnStatusLabel(finalizeCtx, chat, chat.Status, runResult, logger) - server.updateLastTurnSummary(finalizeCtx, chat, chat.HistoryVersion, statusLabel, logger) - server.dispatchSuccessfulTurnPush(finalizeCtx, chat, statusLabel, logger) + server.maybeFinalizeTurnStatusLabelAndPush(finalizeCtx, chat, chat.Status, "", runResult, logger) case runnerActionKindFinishError: server.maybeFinalizeTurnStatusLabelAndPush(context.WithoutCancel(ctx), chat, chat.Status, outcome.LastError, runChatResult{}, logger) case runnerActionKindEnterRequiresAction: diff --git a/coderd/x/chatd/turn_summary_internal_test.go b/coderd/x/chatd/turn_summary_internal_test.go index 3ca6b0741f..9d0abd7004 100644 --- a/coderd/x/chatd/turn_summary_internal_test.go +++ b/coderd/x/chatd/turn_summary_internal_test.go @@ -201,6 +201,82 @@ func TestPendingChatPersistsSummaryButSkipsWebPush(t *testing.T) { require.Equal(t, int32(0), dispatcher.dispatchCount.Load()) } +func TestSuccessfulChildChatOutcomeSkipsSummaryAndWebPush(t *testing.T) { + t.Parallel() + + db, ps := dbtestutil.NewDB(t) + ctx := testutil.Context(t, testutil.WaitMedium) + owner := dbgen.User(t, db, database.User{}) + org := dbgen.Organization(t, db, database.Organization{}) + dbgen.OrganizationMember(t, db, database.OrganizationMember{ + UserID: owner.ID, + OrganizationID: org.ID, + }) + + provider := dbgen.ChatProvider(t, db, database.ChatProvider{ + Provider: "openai", + DisplayName: "OpenAI", + APIKey: "test-key", + Enabled: true, + }) + + modelCfg, err := db.InsertChatModelConfig(ctx, database.InsertChatModelConfigParams{ + AIProviderID: uuid.NullUUID{UUID: provider.ID, Valid: true}, + Provider: "openai", + Model: "test-model", + DisplayName: "Test Model", + CreatedBy: uuid.NullUUID{UUID: owner.ID, Valid: true}, + UpdatedBy: uuid.NullUUID{UUID: owner.ID, Valid: true}, + Enabled: true, + IsDefault: true, + ContextLimit: 128000, + CompressionThreshold: 80, + Options: json.RawMessage(`{}`), + }) + require.NoError(t, err) + + parent, err := db.InsertChat(ctx, database.InsertChatParams{ + OrganizationID: org.ID, + Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, + OwnerID: owner.ID, + LastModelConfigID: modelCfg.ID, + Title: "summary-parent-chat", + MCPServerIDs: []uuid.UUID{}, + }) + require.NoError(t, err) + child, err := db.InsertChat(ctx, database.InsertChatParams{ + OrganizationID: org.ID, + Status: database.ChatStatusWaiting, + ClientType: database.ChatClientTypeUi, + OwnerID: owner.ID, + ParentChatID: uuid.NullUUID{UUID: parent.ID, Valid: true}, + RootChatID: uuid.NullUUID{UUID: parent.ID, Valid: true}, + LastModelConfigID: modelCfg.ID, + Title: "summary-child-chat", + MCPServerIDs: []uuid.UUID{}, + }) + require.NoError(t, err) + + dispatcher := &recordingWebpushDispatcher{} + server := &Server{ + db: db, + pubsub: ps, + logger: slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}), + webpushDispatcher: dispatcher, + } + require.NoError(t, server.afterGenerationOutcome(ctx, generationOutcome{ + Chat: child, + Kind: runnerActionKindFinishTurn, + })) + server.drainInflight() + + fetched, err := db.GetChatByID(ctx, child.ID) + require.NoError(t, err) + require.False(t, fetched.LastTurnSummary.Valid) + require.Equal(t, int32(0), dispatcher.dispatchCount.Load()) +} + type recordingWebpushDispatcher struct { dispatchCount atomic.Int32 }