Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
5d336df
feat: limit concurrent chat agents with pooled admission at acquisition
ibetitsmike Aug 5, 2026
edb9c68
test(coderd): cover FIFO admission, interrupt claim, and concurrent a…
ibetitsmike Aug 5, 2026
ae0122d
refactor(coderd/database): prioritize interrupts and harden candidate…
ibetitsmike Aug 5, 2026
6de8c97
chore(enterprise/coderd/x/chatd): unexport pool caps and use WaitGrou…
ibetitsmike Aug 5, 2026
dee72c0
chore: tighten comments on chatd admission branch
ibetitsmike Aug 5, 2026
0fc32c1
fix(coderd): partition acquisition by capacity pool and add dbauthz t…
ibetitsmike Aug 5, 2026
c822311
fix(coderd/x/chatd): floor acquisition batch size at two
ibetitsmike Aug 5, 2026
161c4bf
fix(coderd/x/chatd): clear persisted queue markers without an admissi…
ibetitsmike Aug 5, 2026
89603b3
docs(coderd/x/chatd): state that runtime-hours usage is not yet popul…
ibetitsmike Aug 5, 2026
1e01395
fix(site): reject stale capacity events and populate db2sdk chat fixture
ibetitsmike Aug 5, 2026
da90da4
fix: count queue entries on marker wins and require newer status events
ibetitsmike Aug 5, 2026
819439a
docs(coderd/x/chatd): correct acquisition ticker default to 1s
ibetitsmike Aug 6, 2026
64d0d69
refactor: derive chat capacity queue state instead of persisting a ma…
ibetitsmike Aug 6, 2026
e52a28d
chore: tighten comments and document capacity gauges
ibetitsmike Aug 6, 2026
b391317
test(coderd/x/chatd): deflake capacity queue event and FIFO tests
ibetitsmike Aug 6, 2026
5d3c88d
fix(coderd/x/chatd): clear queued banner when a different replica admits
ibetitsmike Aug 6, 2026
a36eddb
refactor(coderd): merge chat capacity admission and limits into one seam
ibetitsmike Aug 6, 2026
f54aed7
fix(coderd): clear capacity banner on interrupt and stale capacity ev…
ibetitsmike Aug 6, 2026
07e6306
fix(coderd/x/chatd): publish capacity clear regardless of current cap
ibetitsmike Aug 6, 2026
49beb28
fix(coderd/x/chatd): revalidate capacity queue entry from a fresh sna…
ibetitsmike Aug 6, 2026
e4d3e42
fix(coderd/x/chatd): reconcile unseen capacity queue entries on all-s…
ibetitsmike Aug 6, 2026
ab2a43e
fix(coderd): queue chats arriving behind a full-pool backlog
ibetitsmike Aug 6, 2026
d6c5e60
fix(coderd): close capacity admission bypass and stuck queued banner …
ibetitsmike Aug 6, 2026
6c99ae7
fix(site/src/api/queries): reconcile rejected capacity events via ent…
ibetitsmike Aug 6, 2026
fcefcad
chore: clean up chat capacity comments
ibetitsmike Aug 10, 2026
95806a3
fix: base chat capacity on live ownership
ibetitsmike Aug 10, 2026
e37d268
fix(enterprise/coderd/x/chatd): enforce runtime hard limit
ibetitsmike Aug 11, 2026
8676e86
refactor: simplify chat capacity admission
ibetitsmike Aug 11, 2026
fbb4d32
fix: enforce chat capacity across deployments
ibetitsmike Aug 12, 2026
8bdcf1a
chore: clean up concurrency comments
ibetitsmike Aug 12, 2026
cdadc19
fix(coderd/x/chatd): address review feedback on capacity comments and…
ibetitsmike Aug 12, 2026
84f6666
docs: simplify chatd limiter architecture
ibetitsmike Aug 12, 2026
6d1e03e
test(enterprise/coderd/x/chatd): pin capped unlock for disabled zero-…
jaaydenh Aug 13, 2026
d4e2eee
fix: align pooled chat admission with current main
ibetitsmike Aug 13, 2026
adb7efb
fix(coderd/database): renumber migration to avoid collision with main
ibetitsmike Aug 17, 2026
f56a4e2
fix(site/src/pages/AgentsPage): keep running-chat poll under the bind…
ibetitsmike Aug 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions coderd/apidoc/docs.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions coderd/apidoc/swagger.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions coderd/coderd.go
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,8 @@ type Options struct {
// Set by enterprise for HA deployments. Nil uses chatd's local
// in-process channel dialer.
ChatStreamPartsDialer chatd.StreamPartsDialer
// Nil keeps the default chat agent caps active.
ChatAgentCapacityUnlock chatd.AgentCapacityUnlock
// ChatProviderAPIKeys overrides deployment-derived provider keys.
// Test harnesses use this to route chat models to local providers.
ChatProviderAPIKeys *chatprovider.ProviderAPIKeys
Expand Down Expand Up @@ -941,6 +943,7 @@ func New(options *Options) *API {
HookDispatcher: hookDispatcher,
UsageTracker: options.WorkspaceUsageTracker,
PrometheusRegistry: options.PrometheusRegistry,
AgentCapacityUnlock: options.ChatAgentCapacityUnlock,
OIDCTokenSource: oidcMCPSrc,
NotificationsEnqueuer: options.NotificationsEnqueuer,
Auditor: &api.Auditor,
Expand Down
7 changes: 2 additions & 5 deletions coderd/database/db2sdk/db2sdk_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -757,11 +757,8 @@ func TestChat_AllFieldsPopulated(t *testing.T) {

v := reflect.ValueOf(got)
typ := v.Type()
// HasUnread is populated by ChatRowsWithChildren (which joins the
// read-cursor query), not by Chat. Warnings is a transient
// field populated by handlers, not the converter. Both are
// expected to remain zero here.
skip := map[string]bool{"HasUnread": true, "Warnings": true}
// These fields are set outside db2sdk.Chat and intentionally remain zero.
skip := map[string]bool{"HasUnread": true, "Warnings": true, "QueuedForCapacity": true}
for i := range typ.NumField() {
field := typ.Field(i)
if skip[field.Name] {
Expand Down
23 changes: 23 additions & 0 deletions coderd/database/dbauthz/dbauthz.go
Original file line number Diff line number Diff line change
Expand Up @@ -1964,6 +1964,20 @@ func (q *querier) CountAuditLogs(ctx context.Context, arg database.CountAuditLog
return q.db.CountAuthorizedAuditLogs(ctx, arg, prep)
}

func (q *querier) CountChatCapacityActiveByPool(ctx context.Context, arg database.CountChatCapacityActiveByPoolParams) (database.CountChatCapacityActiveByPoolRow, error) {
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceChat); err != nil {
return database.CountChatCapacityActiveByPoolRow{}, err
}
return q.db.CountChatCapacityActiveByPool(ctx, arg)
}

func (q *querier) CountChatCapacityQueuedByPool(ctx context.Context, staleSeconds int32) (database.CountChatCapacityQueuedByPoolRow, error) {
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceChat); err != nil {
return database.CountChatCapacityQueuedByPoolRow{}, err
}
return q.db.CountChatCapacityQueuedByPool(ctx, staleSeconds)
}

func (q *querier) CountChatQueuedMessages(ctx context.Context, chatID uuid.UUID) (int64, error) {
_, err := q.GetChatByID(ctx, chatID)
if err != nil {
Expand Down Expand Up @@ -3483,6 +3497,15 @@ func (q *querier) GetChatPlanModeInstructions(ctx context.Context) (string, erro
return q.db.GetChatPlanModeInstructions(ctx)
}

func (q *querier) GetChatQueuedForCapacity(ctx context.Context, arg database.GetChatQueuedForCapacityParams) (bool, error) {
// The pool-fullness derivation counts other users' chats, so require
// deployment-wide chat read rather than per-chat authorization.
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceChat); err != nil {
return false, err
}
return q.db.GetChatQueuedForCapacity(ctx, arg)
}

func (q *querier) GetChatQueuedMessageByID(ctx context.Context, arg database.GetChatQueuedMessageByIDParams) (database.ChatQueuedMessage, error) {
_, err := q.GetChatByID(ctx, arg.ChatID)
if err != nil {
Expand Down
17 changes: 17 additions & 0 deletions coderd/database/dbauthz/dbauthz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,23 @@ func (s *MethodTestSuite) TestChats() {
dbm.EXPECT().GetChatWorkerAcquisitionCandidates(gomock.Any(), arg).Return([]database.GetChatWorkerAcquisitionCandidatesRow{row}, nil).AnyTimes()
check.Args(arg).Asserts(rbac.ResourceChat, policy.ActionUpdate).Returns([]database.GetChatWorkerAcquisitionCandidatesRow{row})
}))
s.Run("CountChatCapacityActiveByPool", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
arg := database.CountChatCapacityActiveByPoolParams{ExcludeChatID: uuid.New(), StaleSeconds: 30}
row := database.CountChatCapacityActiveByPoolRow{ActiveRootCount: 1, ActiveSubagentCount: 2}
dbm.EXPECT().CountChatCapacityActiveByPool(gomock.Any(), arg).Return(row, nil).AnyTimes()
check.Args(arg).Asserts(rbac.ResourceChat, policy.ActionRead).Returns(row)
}))
s.Run("CountChatCapacityQueuedByPool", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
const staleSeconds = int32(30)
row := database.CountChatCapacityQueuedByPoolRow{QueuedRootCount: 3, QueuedSubagentCount: 4}
dbm.EXPECT().CountChatCapacityQueuedByPool(gomock.Any(), staleSeconds).Return(row, nil).AnyTimes()
check.Args(staleSeconds).Asserts(rbac.ResourceChat, policy.ActionRead).Returns(row)
}))
s.Run("GetChatQueuedForCapacity", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
arg := database.GetChatQueuedForCapacityParams{ChatID: uuid.New(), StaleSeconds: 30, RootCapacity: 5, SubagentCapacity: 10}
dbm.EXPECT().GetChatQueuedForCapacity(gomock.Any(), arg).Return(true, nil).AnyTimes()
check.Args(arg).Asserts(rbac.ResourceChat, policy.ActionRead).Returns(true)
}))
s.Run("GetChatsByIDsForRunnerSync", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
ids := []uuid.UUID{uuid.New(), uuid.New()}
chat := testutil.Fake(s.T(), faker, database.Chat{ID: ids[0]})
Expand Down
24 changes: 24 additions & 0 deletions coderd/database/dbmetrics/querymetrics.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

45 changes: 45 additions & 0 deletions coderd/database/dbmock/dbmock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion coderd/database/dump.sql

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions coderd/database/lock.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const (
LockIDBoundaryUsageStats
LockIDAIProvidersEnvSeed
LockIDChatModelConfigWrites
LockIDChatCapacityAdmission
)

// GenLockID generates a unique and consistent lock ID from a given string.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
DROP INDEX idx_chats_worker_acquisition_candidates;
CREATE INDEX idx_chats_worker_acquisition_candidates ON chats
(status, updated_at, id)
WHERE archived = false;
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
DROP INDEX idx_chats_worker_acquisition_candidates;
CREATE INDEX idx_chats_worker_acquisition_candidates ON chats
((parent_chat_id IS NULL), status, updated_at, id)
WHERE archived = false;
19 changes: 8 additions & 11 deletions coderd/database/querier.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading