diff --git a/AGENTS.md b/AGENTS.md index 0d0f9635c0..3428b7a315 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -128,6 +128,18 @@ app, err := api.Database.GetOAuth2ProviderAppByClientID(ctx, clientID) - Keep nullable-field handling, type coercion, and response shaping in the converter so handlers stay focused on request flow and authorization. +### Transactions and `InTx` + +- Inside `db.InTx(...)` closures, do not use the outer store (`api.Database`, + `p.db`, etc.) directly or indirectly. Use the `tx` handle for DB work inside + the closure, or fetch read-only inputs before opening the transaction. +- Watch for helper methods on a receiver that hide outer-store access. A call + like `p.someHelper(ctx)` is still unsafe inside `InTx` if that helper uses + `p.db` internally. +- Using the outer store while a transaction is open can hold one connection and + then block on another pool checkout, which can cause pool starvation and + `idle in transaction` incidents under load. + ## Quick Reference ### Full workflows available in imported WORKFLOWS.md diff --git a/coderd/x/chatd/chatd.go b/coderd/x/chatd/chatd.go index 2a4ab237f0..474675476d 100644 --- a/coderd/x/chatd/chatd.go +++ b/coderd/x/chatd/chatd.go @@ -875,6 +875,10 @@ func (p *Server) CreateChat(ctx context.Context, opts CreateOptions) (database.C if opts.Labels == nil { opts.Labels = database.StringMap{} } + // Resolve the deployment prompt before opening the transaction so + // chat creation does not hold one DB connection while waiting for + // another pool checkout. + deploymentPrompt := p.resolveDeploymentSystemPrompt(ctx) var chat database.Chat txErr := p.db.InTx(func(tx database.Store) error { @@ -915,7 +919,6 @@ func (p *Server) CreateChat(ctx context.Context, opts CreateOptions) (database.C return xerrors.Errorf("insert chat: %w", err) } - deploymentPrompt := p.resolveDeploymentSystemPrompt(ctx) userPrompt := SanitizePromptText(opts.SystemPrompt) var workspaceAwareness string if opts.WorkspaceID.Valid { diff --git a/coderd/x/chatd/subagent.go b/coderd/x/chatd/subagent.go index 4792df2b66..25620aa5ef 100644 --- a/coderd/x/chatd/subagent.go +++ b/coderd/x/chatd/subagent.go @@ -452,6 +452,10 @@ func (p *Server) createChildSubagentChatWithOptions( return database.Chat{}, xerrors.Errorf("marshal labels: %w", err) } childSystemPrompt := SanitizePromptText(opts.systemPrompt) + // Resolve the deployment prompt before opening the transaction so + // child chat creation does not hold one DB connection while waiting + // for another pool checkout. + deploymentPrompt := p.resolveDeploymentSystemPrompt(ctx) var child database.Chat txErr := p.db.InTx(func(tx database.Store) error { @@ -482,7 +486,6 @@ func (p *Server) createChildSubagentChatWithOptions( return xerrors.Errorf("insert child chat: %w", err) } - deploymentPrompt := p.resolveDeploymentSystemPrompt(ctx) workspaceAwareness := "There is no workspace associated with this chat yet. Create one using the create_workspace tool before using workspace tools like execute, read_file, write_file, etc." if insertedChat.WorkspaceID.Valid { workspaceAwareness = "This chat is attached to a workspace. You can use workspace tools like execute, read_file, write_file, etc."