From 832dc6784addcc708560a4280092eb68c1643251 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Mon, 15 Jun 2026 18:21:35 +0000 Subject: [PATCH] feat: add chat context pinning storage and push trigger Foundation for the Workspace Context Sources RFC phase 3. Agent push (#25983) and coderd snapshot storage (#26145) persist per-agent context snapshots; this lands the chat-side storage plus the agentapi trigger that a follow-up will use to read them. It is inert: nothing wires an implementation yet, so behavior is unchanged and chatd is untouched. Add four nullable columns to chats (context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error) and rebuild the chats_expanded view. Add three queries -- SetChatContextSnapshot, HydrateAgentChatsContext, MarkChatsContextDirtyByAgent -- with dbauthz wrappers and audit entries. The queries have no production callers yet; they are store-interface methods covered by a Postgres test. Add the agentapi ContextDirtyMarker interface and invoke it inside the PushContextState transaction, publishing collected events only after commit. No implementation is wired in this PR, so the trigger is dormant. The full context integration (the chatd implementation, prompt consumption, SDK/UI, and retiring the per-turn pull) lands separately. Refs #25983, #26145. --- coderd/agentapi/api.go | 26 +- coderd/agentapi/context.go | 41 ++ coderd/agentapi/context_test.go | 83 ++++ coderd/database/dbauthz/dbauthz.go | 30 ++ coderd/database/dbauthz/dbauthz_test.go | 18 + coderd/database/dbmetrics/querymetrics.go | 24 + coderd/database/dbmock/dbmock.go | 43 ++ coderd/database/dump.sql | 18 +- .../000523_chat_context_hydration.down.sql | 54 ++ .../000523_chat_context_hydration.up.sql | 70 +++ coderd/database/modelqueries.go | 10 +- coderd/database/models.go | 12 + coderd/database/querier.go | 16 + coderd/database/querier_test.go | 116 +++++ coderd/database/queries.sql.go | 460 +++++++++++++++--- coderd/database/queries/chats.sql | 161 +++++- docs/admin/security/audit-logs.md | 70 +-- enterprise/audit/table.go | 4 + 18 files changed, 1116 insertions(+), 140 deletions(-) create mode 100644 coderd/database/migrations/000523_chat_context_hydration.down.sql create mode 100644 coderd/database/migrations/000523_chat_context_hydration.up.sql diff --git a/coderd/agentapi/api.go b/coderd/agentapi/api.go index 5770b8b8d783c..ce697bc4826fe 100644 --- a/coderd/agentapi/api.go +++ b/coderd/agentapi/api.go @@ -75,12 +75,15 @@ type Options struct { OrganizationID uuid.UUID TemplateVersionID uuid.UUID - AuthenticatedCtx context.Context - Log slog.Logger - Clock quartz.Clock - Database database.Store - NotificationsEnqueuer notifications.Enqueuer - Pubsub pubsub.Pubsub + AuthenticatedCtx context.Context + Log slog.Logger + Clock quartz.Clock + Database database.Store + NotificationsEnqueuer notifications.Enqueuer + Pubsub pubsub.Pubsub + // ContextDirtyMarker is the chatd-backed hydrate/dirty fan-out invoked + // from PushContextState. Nil when chatd is disabled. + ContextDirtyMarker ContextDirtyMarker ConnectionLogger *atomic.Pointer[connectionlog.ConnectionLogger] DerpMapFn func() *tailcfg.DERPMap TailnetCoordinator *atomic.Pointer[tailnet.Coordinator] @@ -248,11 +251,12 @@ func New(opts Options, workspace database.Workspace, agent database.WorkspaceAge } api.ContextAPI = &ContextAPI{ - AgentID: agent.ID, - Workspace: api.cachedWorkspaceFields, - Log: opts.Log, - Clock: opts.Clock, - Database: opts.Database, + AgentID: agent.ID, + Workspace: api.cachedWorkspaceFields, + Log: opts.Log, + Clock: opts.Clock, + Database: opts.Database, + DirtyMarker: opts.ContextDirtyMarker, } // Start background cache refresh loop to handle workspace changes diff --git a/coderd/agentapi/context.go b/coderd/agentapi/context.go index 97396005f4831..605635915a19d 100644 --- a/coderd/agentapi/context.go +++ b/coderd/agentapi/context.go @@ -6,6 +6,7 @@ import ( "errors" "math" "sort" + "time" "github.com/google/uuid" "golang.org/x/xerrors" @@ -62,6 +63,25 @@ type ContextAPI struct { Log slog.Logger Clock quartz.Clock Database database.Store + // DirtyMarker hydrates chats from, and marks chats dirty against, the + // snapshot persisted by a push. It is nil when chatd is not running, + // in which case PushContextState stays a pure write path. + DirtyMarker ContextDirtyMarker +} + +// ContextDirtyMarker hydrates chats from, and marks chats dirty against, a +// freshly persisted agent context snapshot. It is implemented by chatd and +// injected at coderd construction so this package neither imports the chat +// domain nor performs chat-authorized writes directly. +type ContextDirtyMarker interface { + // HydrateAndMarkChatsDirty runs inside the PushContextState + // transaction using the supplied store. It hydrates chats for the + // agent that have no pinned hash yet (no dirty event) and flips + // already-pinned chats whose hash differs from aggregateHash. It + // returns a callback that publishes the resulting dirty watch events; + // the caller invokes it only after the transaction commits. The + // callback is nil when nothing transitioned to dirty. + HydrateAndMarkChatsDirty(ctx context.Context, tx database.Store, agentID uuid.UUID, aggregateHash []byte, snapshotError string, now time.Time) (publishDirty func(), err error) } // PushContextState persists a snapshot pushed by the workspace @@ -120,10 +140,15 @@ func (a *ContextAPI) PushContextState(ctx context.Context, req *agentproto.PushC sort.Strings(activeSources) var accepted bool + // publishDirty is captured from the final (committed) attempt and + // invoked after the transaction commits; ReadModifyUpdate may re-run + // the closure on serialization conflicts. + var publishDirty func() err = database.ReadModifyUpdate(a.Database, func(tx database.Store) error { // The closure re-runs on serialization conflicts; reset any // state carried over from a rolled-back attempt. accepted = false + publishDirty = nil existing, err := tx.GetLatestWorkspaceAgentContextSnapshot(ctx, a.AgentID) switch { @@ -171,6 +196,16 @@ func (a *ContextAPI) PushContextState(ctx context.Context, req *agentproto.PushC return xerrors.Errorf("delete stale resources: %w", err) } + // Hydrate and dirty chats against the snapshot just written, in the + // same transaction so a concurrent refresh cannot interleave with + // the version gate. Events are published only after commit. + if a.DirtyMarker != nil { + publishDirty, err = a.DirtyMarker.HydrateAndMarkChatsDirty(ctx, tx, a.AgentID, req.AggregateHash, req.SnapshotError, now) + if err != nil { + return xerrors.Errorf("hydrate and mark chats dirty: %w", err) + } + } + accepted = true return nil }) @@ -187,6 +222,12 @@ func (a *ContextAPI) PushContextState(ctx context.Context, req *agentproto.PushC return &agentproto.PushContextStateResponse{Accepted: false}, nil } + // The snapshot committed; fan out dirty watch events to chats whose + // pinned context drifted from this push. + if publishDirty != nil { + publishDirty() + } + a.Log.Debug(ctx, "PushContextState accepted", slog.F("agent_id", a.AgentID), slog.F("version", req.Version), diff --git a/coderd/agentapi/context_test.go b/coderd/agentapi/context_test.go index 522785da90828..5c724b93560da 100644 --- a/coderd/agentapi/context_test.go +++ b/coderd/agentapi/context_test.go @@ -88,6 +88,70 @@ func TestPushContextState(t *testing.T) { require.True(t, resp.GetAccepted()) }) + t.Run("DirtyMarkerInvokedAfterCommit", func(t *testing.T) { + t.Parallel() + + api, dbm := makeAPI(t) + marker := &fakeDirtyMarker{} + api.DirtyMarker = marker + expectInTx(dbm) + + dbm.EXPECT().GetLatestWorkspaceAgentContextSnapshot(gomock.Any(), agentID). + Return(database.WorkspaceAgentContextSnapshot{}, errNoRows()) + dbm.EXPECT().UpsertWorkspaceAgentContextSnapshot(gomock.Any(), gomock.Any()). + Return(database.WorkspaceAgentContextSnapshot{}, nil) + dbm.EXPECT().UpsertWorkspaceAgentContextResource(gomock.Any(), gomock.Any()). + Return(database.WorkspaceAgentContextResource{}, nil).Times(1) + dbm.EXPECT().DeleteStaleWorkspaceAgentContextResources(gomock.Any(), gomock.Any()). + Return(nil) + + resp, err := api.PushContextState(context.Background(), &agentproto.PushContextStateRequest{ + Version: 1, + AggregateHash: []byte{0xaa, 0xbb}, + SnapshotError: "watcher degraded", + Initial: true, + Resources: []*agentproto.ContextResource{ + instructionResource("/home/coder/AGENTS.md", "hello"), + }, + }) + require.NoError(t, err) + require.True(t, resp.GetAccepted()) + // The marker runs inside the push transaction and its returned + // callback publishes only after the transaction commits. + require.Equal(t, 1, marker.called) + require.Equal(t, 1, marker.published) + require.Equal(t, agentID, marker.gotAgent) + require.Equal(t, []byte{0xaa, 0xbb}, marker.gotHash) + require.Equal(t, "watcher degraded", marker.gotErr) + }) + + t.Run("DirtyMarkerSkippedOnDrop", func(t *testing.T) { + t.Parallel() + + api, dbm := makeAPI(t) + marker := &fakeDirtyMarker{} + api.DirtyMarker = marker + expectInTx(dbm) + + // A non-initial push at a version not strictly greater than the + // stored one is dropped before any write; hydration and the + // dirty fan-out must not run. + dbm.EXPECT().GetLatestWorkspaceAgentContextSnapshot(gomock.Any(), agentID). + Return(database.WorkspaceAgentContextSnapshot{Version: 5}, nil) + + resp, err := api.PushContextState(context.Background(), &agentproto.PushContextStateRequest{ + Version: 2, + AggregateHash: []byte{0x01}, + Resources: []*agentproto.ContextResource{ + instructionResource("/home/coder/AGENTS.md", "hello"), + }, + }) + require.NoError(t, err) + require.False(t, resp.GetAccepted()) + require.Equal(t, 0, marker.called) + require.Equal(t, 0, marker.published) + }) + t.Run("RejectsEmptyAndDuplicateSources", func(t *testing.T) { t.Parallel() @@ -598,3 +662,22 @@ func mcpServerResource(source, serverName, description string) *agentproto.Conte }, } } + +// fakeDirtyMarker is a test double for agentapi.ContextDirtyMarker. It records +// the in-transaction call and counts callback invocations so tests can assert +// the marker runs inside the push transaction and publishes only after commit. +type fakeDirtyMarker struct { + called int + published int + gotAgent uuid.UUID + gotHash []byte + gotErr string +} + +func (f *fakeDirtyMarker) HydrateAndMarkChatsDirty(_ context.Context, _ database.Store, agentID uuid.UUID, aggregateHash []byte, snapshotError string, _ time.Time) (func(), error) { + f.called++ + f.gotAgent = agentID + f.gotHash = aggregateHash + f.gotErr = snapshotError + return func() { f.published++ }, nil +} diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 201a96aa19362..39edc4a8b662f 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -5660,6 +5660,16 @@ func (q *querier) GetWorkspacesForWorkspaceMetrics(ctx context.Context) ([]datab return q.db.GetWorkspacesForWorkspaceMetrics(ctx) } +func (q *querier) HydrateAgentChatsContext(ctx context.Context, arg database.HydrateAgentChatsContextParams) error { + // System-level operation: an agent context push fans hydration out + // across every not-yet-pinned chat for the agent, so it authorizes at + // the resource level rather than per-chat. + if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceChat); err != nil { + return err + } + return q.db.HydrateAgentChatsContext(ctx, arg) +} + func (q *querier) IncrementChatGenerationAttempt(ctx context.Context, id uuid.UUID) (int64, error) { chat, err := q.db.GetChatByID(ctx, id) if err != nil { @@ -6642,6 +6652,15 @@ func (q *querier) MarkAllInboxNotificationsAsRead(ctx context.Context, arg datab return q.db.MarkAllInboxNotificationsAsRead(ctx, arg) } +func (q *querier) MarkChatsContextDirtyByAgent(ctx context.Context, arg database.MarkChatsContextDirtyByAgentParams) ([]database.MarkChatsContextDirtyByAgentRow, error) { + // System-level operation: the dirty fan-out runs across every active + // chat for the agent in response to a context push. + if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceChat); err != nil { + return nil, err + } + return q.db.MarkChatsContextDirtyByAgent(ctx, arg) +} + func (q *querier) OIDCClaimFieldValues(ctx context.Context, args database.OIDCClaimFieldValuesParams) ([]string, error) { resource := rbac.ResourceIdpsyncSettings if args.OrganizationID != uuid.Nil { @@ -6772,6 +6791,17 @@ func (q *querier) SelectUsageEventsForPublishing(ctx context.Context, arg time.T return q.db.SelectUsageEventsForPublishing(ctx, arg) } +func (q *querier) SetChatContextSnapshot(ctx context.Context, arg database.SetChatContextSnapshotParams) error { + chat, err := q.db.GetChatByID(ctx, arg.ID) + if err != nil { + return err + } + if err := q.authorizeContext(ctx, policy.ActionUpdate, chat); err != nil { + return err + } + return q.db.SetChatContextSnapshot(ctx, arg) +} + func (q *querier) SoftDeleteChatMessageByID(ctx context.Context, id int64) error { msg, err := q.db.GetChatMessageByID(ctx, id) if err != nil { diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index b8a0422cecc15..91bacb9bb5a30 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -529,6 +529,24 @@ func (s *MethodTestSuite) TestChats() { dbm.EXPECT().AcquireChats(gomock.Any(), arg).Return([]database.Chat{chat}, nil).AnyTimes() check.Args(arg).Asserts(rbac.ResourceChat, policy.ActionUpdate).Returns([]database.Chat{chat}) })) + s.Run("HydrateAgentChatsContext", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { + arg := database.HydrateAgentChatsContextParams{AgentID: uuid.New()} + dbm.EXPECT().HydrateAgentChatsContext(gomock.Any(), arg).Return(nil).AnyTimes() + check.Args(arg).Asserts(rbac.ResourceChat, policy.ActionUpdate) + })) + s.Run("MarkChatsContextDirtyByAgent", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { + arg := database.MarkChatsContextDirtyByAgentParams{AgentID: uuid.New()} + rows := []database.MarkChatsContextDirtyByAgentRow{{ID: uuid.New(), OwnerID: uuid.New()}} + dbm.EXPECT().MarkChatsContextDirtyByAgent(gomock.Any(), arg).Return(rows, nil).AnyTimes() + check.Args(arg).Asserts(rbac.ResourceChat, policy.ActionUpdate).Returns(rows) + })) + s.Run("SetChatContextSnapshot", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { + chat := testutil.Fake(s.T(), faker, database.Chat{}) + arg := database.SetChatContextSnapshotParams{ID: chat.ID} + dbm.EXPECT().GetChatByID(gomock.Any(), chat.ID).Return(chat, nil).AnyTimes() + dbm.EXPECT().SetChatContextSnapshot(gomock.Any(), arg).Return(nil).AnyTimes() + check.Args(arg).Asserts(chat, policy.ActionUpdate) + })) s.Run("GetChatWorkerAcquisitionCandidates", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { arg := database.GetChatWorkerAcquisitionCandidatesParams{ StaleSeconds: 30, diff --git a/coderd/database/dbmetrics/querymetrics.go b/coderd/database/dbmetrics/querymetrics.go index 808a6341e71fa..3758bcb535588 100644 --- a/coderd/database/dbmetrics/querymetrics.go +++ b/coderd/database/dbmetrics/querymetrics.go @@ -3874,6 +3874,14 @@ func (m queryMetricsStore) GetWorkspacesForWorkspaceMetrics(ctx context.Context) return r0, r1 } +func (m queryMetricsStore) HydrateAgentChatsContext(ctx context.Context, arg database.HydrateAgentChatsContextParams) error { + start := time.Now() + r0 := m.s.HydrateAgentChatsContext(ctx, arg) + m.queryLatencies.WithLabelValues("HydrateAgentChatsContext").Observe(time.Since(start).Seconds()) + m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "HydrateAgentChatsContext").Inc() + return r0 +} + func (m queryMetricsStore) IncrementChatGenerationAttempt(ctx context.Context, id uuid.UUID) (int64, error) { start := time.Now() r0, r1 := m.s.IncrementChatGenerationAttempt(ctx, id) @@ -4762,6 +4770,14 @@ func (m queryMetricsStore) MarkAllInboxNotificationsAsRead(ctx context.Context, return r0 } +func (m queryMetricsStore) MarkChatsContextDirtyByAgent(ctx context.Context, arg database.MarkChatsContextDirtyByAgentParams) ([]database.MarkChatsContextDirtyByAgentRow, error) { + start := time.Now() + r0, r1 := m.s.MarkChatsContextDirtyByAgent(ctx, arg) + m.queryLatencies.WithLabelValues("MarkChatsContextDirtyByAgent").Observe(time.Since(start).Seconds()) + m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "MarkChatsContextDirtyByAgent").Inc() + return r0, r1 +} + func (m queryMetricsStore) OIDCClaimFieldValues(ctx context.Context, arg database.OIDCClaimFieldValuesParams) ([]string, error) { start := time.Now() r0, r1 := m.s.OIDCClaimFieldValues(ctx, arg) @@ -4874,6 +4890,14 @@ func (m queryMetricsStore) SelectUsageEventsForPublishing(ctx context.Context, n return r0, r1 } +func (m queryMetricsStore) SetChatContextSnapshot(ctx context.Context, arg database.SetChatContextSnapshotParams) error { + start := time.Now() + r0 := m.s.SetChatContextSnapshot(ctx, arg) + m.queryLatencies.WithLabelValues("SetChatContextSnapshot").Observe(time.Since(start).Seconds()) + m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "SetChatContextSnapshot").Inc() + return r0 +} + func (m queryMetricsStore) SoftDeleteChatMessageByID(ctx context.Context, id int64) error { start := time.Now() r0 := m.s.SoftDeleteChatMessageByID(ctx, id) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index 1c66533a76701..d9807d3cbb0c3 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -7243,6 +7243,20 @@ func (mr *MockStoreMockRecorder) GetWorkspacesForWorkspaceMetrics(ctx any) *gomo return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspacesForWorkspaceMetrics", reflect.TypeOf((*MockStore)(nil).GetWorkspacesForWorkspaceMetrics), ctx) } +// HydrateAgentChatsContext mocks base method. +func (m *MockStore) HydrateAgentChatsContext(ctx context.Context, arg database.HydrateAgentChatsContextParams) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "HydrateAgentChatsContext", ctx, arg) + ret0, _ := ret[0].(error) + return ret0 +} + +// HydrateAgentChatsContext indicates an expected call of HydrateAgentChatsContext. +func (mr *MockStoreMockRecorder) HydrateAgentChatsContext(ctx, arg any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HydrateAgentChatsContext", reflect.TypeOf((*MockStore)(nil).HydrateAgentChatsContext), ctx, arg) +} + // InTx mocks base method. func (m *MockStore) InTx(arg0 func(database.Store) error, arg1 *database.TxOptions) error { m.ctrl.T.Helper() @@ -8966,6 +8980,21 @@ func (mr *MockStoreMockRecorder) MarkAllInboxNotificationsAsRead(ctx, arg any) * return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MarkAllInboxNotificationsAsRead", reflect.TypeOf((*MockStore)(nil).MarkAllInboxNotificationsAsRead), ctx, arg) } +// MarkChatsContextDirtyByAgent mocks base method. +func (m *MockStore) MarkChatsContextDirtyByAgent(ctx context.Context, arg database.MarkChatsContextDirtyByAgentParams) ([]database.MarkChatsContextDirtyByAgentRow, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "MarkChatsContextDirtyByAgent", ctx, arg) + ret0, _ := ret[0].([]database.MarkChatsContextDirtyByAgentRow) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// MarkChatsContextDirtyByAgent indicates an expected call of MarkChatsContextDirtyByAgent. +func (mr *MockStoreMockRecorder) MarkChatsContextDirtyByAgent(ctx, arg any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MarkChatsContextDirtyByAgent", reflect.TypeOf((*MockStore)(nil).MarkChatsContextDirtyByAgent), ctx, arg) +} + // OIDCClaimFieldValues mocks base method. func (m *MockStore) OIDCClaimFieldValues(ctx context.Context, arg database.OIDCClaimFieldValuesParams) ([]string, error) { m.ctrl.T.Helper() @@ -9203,6 +9232,20 @@ func (mr *MockStoreMockRecorder) SelectUsageEventsForPublishing(ctx, now any) *g return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SelectUsageEventsForPublishing", reflect.TypeOf((*MockStore)(nil).SelectUsageEventsForPublishing), ctx, now) } +// SetChatContextSnapshot mocks base method. +func (m *MockStore) SetChatContextSnapshot(ctx context.Context, arg database.SetChatContextSnapshotParams) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetChatContextSnapshot", ctx, arg) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetChatContextSnapshot indicates an expected call of SetChatContextSnapshot. +func (mr *MockStoreMockRecorder) SetChatContextSnapshot(ctx, arg any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetChatContextSnapshot", reflect.TypeOf((*MockStore)(nil).SetChatContextSnapshot), ctx, arg) +} + // SoftDeleteChatMessageByID mocks base method. func (m *MockStore) SoftDeleteChatMessageByID(ctx context.Context, id int64) error { m.ctrl.T.Helper() diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 96ed194d8e34e..f08d96e4fab38 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -1969,6 +1969,10 @@ CREATE TABLE chats ( retry_state_version bigint DEFAULT 0 NOT NULL, runner_id uuid, requires_action_deadline_at timestamp with time zone, + context_aggregate_hash bytea, + context_dirty_since timestamp with time zone, + context_dirty_resources jsonb, + context_error text DEFAULT ''::text NOT NULL, CONSTRAINT chat_acl_only_on_root_chats CHECK ((((parent_chat_id IS NULL) AND (root_chat_id IS NULL)) OR ((user_acl = '{}'::jsonb) AND (group_acl = '{}'::jsonb)))), CONSTRAINT chat_group_acl_not_null_jsonb CHECK (((group_acl IS NOT NULL) AND (jsonb_typeof(group_acl) = 'object'::text))), CONSTRAINT chat_user_acl_not_null_jsonb CHECK (((user_acl IS NOT NULL) AND (jsonb_typeof(user_acl) = 'object'::text))), @@ -1982,6 +1986,14 @@ COMMENT ON COLUMN chats.history_version IS 'Snapshot version of the latest durab COMMENT ON COLUMN chats.queue_version IS 'Snapshot version of the latest queued-message change. Starts at 0 until chat_queued_messages triggers set it to the current snapshot_version.'; +COMMENT ON COLUMN chats.context_aggregate_hash IS 'Aggregate hash of the agent context snapshot this chat is pinned to. NULL until first hydrated; compared against the agent''s latest snapshot hash to detect drift.'; + +COMMENT ON COLUMN chats.context_dirty_since IS 'Set when an agent push changes the pinned hash; cleared on refresh. NULL means clean.'; + +COMMENT ON COLUMN chats.context_dirty_resources IS 'Deterministic prefix of resources that changed since the pinned hash. Reserved for the dirty diff; left NULL until the UI phase populates it.'; + +COMMENT ON COLUMN chats.context_error IS 'Snapshot-level error copied from the pinned snapshot (count cap exceeded, watcher degraded, etc.). Empty when healthy.'; + CREATE TABLE users ( id uuid NOT NULL, email text NOT NULL, @@ -2073,7 +2085,11 @@ CREATE VIEW chats_expanded AS COALESCE(root.user_acl, c.user_acl) AS user_acl, COALESCE(root.group_acl, c.group_acl) AS group_acl, owner.username AS owner_username, - owner.name AS owner_name + owner.name AS owner_name, + c.context_aggregate_hash, + c.context_dirty_since, + c.context_dirty_resources, + c.context_error FROM ((chats c LEFT JOIN chats root ON ((root.id = COALESCE(c.root_chat_id, c.parent_chat_id)))) JOIN visible_users owner ON ((owner.id = c.owner_id))); diff --git a/coderd/database/migrations/000523_chat_context_hydration.down.sql b/coderd/database/migrations/000523_chat_context_hydration.down.sql new file mode 100644 index 0000000000000..8871ccf81ea2f --- /dev/null +++ b/coderd/database/migrations/000523_chat_context_hydration.down.sql @@ -0,0 +1,54 @@ +-- Recreate chats_expanded without the new chat columns. The view must +-- be dropped before the columns it references can be removed. +DROP VIEW IF EXISTS chats_expanded; + +ALTER TABLE chats + DROP COLUMN IF EXISTS context_aggregate_hash, + DROP COLUMN IF EXISTS context_dirty_since, + DROP COLUMN IF EXISTS context_dirty_resources, + DROP COLUMN IF EXISTS context_error; + +CREATE VIEW chats_expanded AS + SELECT c.id, + c.owner_id, + c.workspace_id, + c.title, + c.status, + c.worker_id, + c.started_at, + c.heartbeat_at, + c.created_at, + c.updated_at, + c.parent_chat_id, + c.root_chat_id, + c.last_model_config_id, + c.archived, + c.last_error, + c.mode, + c.mcp_server_ids, + c.labels, + c.build_id, + c.agent_id, + c.pin_order, + c.last_read_message_id, + c.last_injected_context, + c.dynamic_tools, + c.organization_id, + c.plan_mode, + c.client_type, + c.last_turn_summary, + c.snapshot_version, + c.history_version, + c.queue_version, + c.generation_attempt, + c.retry_state, + c.retry_state_version, + c.runner_id, + c.requires_action_deadline_at, + COALESCE(root.user_acl, c.user_acl) AS user_acl, + COALESCE(root.group_acl, c.group_acl) AS group_acl, + owner.username AS owner_username, + owner.name AS owner_name + FROM ((chats c + LEFT JOIN chats root ON ((root.id = COALESCE(c.root_chat_id, c.parent_chat_id)))) + JOIN visible_users owner ON ((owner.id = c.owner_id))); diff --git a/coderd/database/migrations/000523_chat_context_hydration.up.sql b/coderd/database/migrations/000523_chat_context_hydration.up.sql new file mode 100644 index 0000000000000..eba5226041a11 --- /dev/null +++ b/coderd/database/migrations/000523_chat_context_hydration.up.sql @@ -0,0 +1,70 @@ +-- Chat-side pin of the agent's latest pushed context snapshot +-- (workspace_agent_context_snapshots). Written by hydration (chat +-- create and agent push) and the dirty fan-out, and re-pinned by the +-- refresh endpoint. These columns are dark plumbing: they do not feed +-- prompt building and the per-turn context pull is unchanged. They are +-- read by drift detection and the refresh endpoint only. +ALTER TABLE chats + ADD COLUMN context_aggregate_hash bytea, + ADD COLUMN context_dirty_since timestamptz, + ADD COLUMN context_dirty_resources jsonb, + ADD COLUMN context_error text NOT NULL DEFAULT ''; + +COMMENT ON COLUMN chats.context_aggregate_hash IS 'Aggregate hash of the agent context snapshot this chat is pinned to. NULL until first hydrated; compared against the agent''s latest snapshot hash to detect drift.'; +COMMENT ON COLUMN chats.context_dirty_since IS 'Set when an agent push changes the pinned hash; cleared on refresh. NULL means clean.'; +COMMENT ON COLUMN chats.context_dirty_resources IS 'Deterministic prefix of resources that changed since the pinned hash. Reserved for the dirty diff; left NULL until the UI phase populates it.'; +COMMENT ON COLUMN chats.context_error IS 'Snapshot-level error copied from the pinned snapshot (count cap exceeded, watcher degraded, etc.). Empty when healthy.'; + +-- Refresh chats_expanded to include the new chat columns. The gentest +-- TestViewSubsetChat requires every chats column to appear in the view. +-- Drop and recreate because a view cannot have columns inserted in the +-- middle of its column list. +DROP VIEW IF EXISTS chats_expanded; +CREATE VIEW chats_expanded AS + SELECT c.id, + c.owner_id, + c.workspace_id, + c.title, + c.status, + c.worker_id, + c.started_at, + c.heartbeat_at, + c.created_at, + c.updated_at, + c.parent_chat_id, + c.root_chat_id, + c.last_model_config_id, + c.archived, + c.last_error, + c.mode, + c.mcp_server_ids, + c.labels, + c.build_id, + c.agent_id, + c.pin_order, + c.last_read_message_id, + c.last_injected_context, + c.dynamic_tools, + c.organization_id, + c.plan_mode, + c.client_type, + c.last_turn_summary, + c.snapshot_version, + c.history_version, + c.queue_version, + c.generation_attempt, + c.retry_state, + c.retry_state_version, + c.runner_id, + c.requires_action_deadline_at, + COALESCE(root.user_acl, c.user_acl) AS user_acl, + COALESCE(root.group_acl, c.group_acl) AS group_acl, + owner.username AS owner_username, + owner.name AS owner_name, + c.context_aggregate_hash, + c.context_dirty_since, + c.context_dirty_resources, + c.context_error + FROM ((chats c + LEFT JOIN chats root ON ((root.id = COALESCE(c.root_chat_id, c.parent_chat_id)))) + JOIN visible_users owner ON ((owner.id = c.owner_id))); diff --git a/coderd/database/modelqueries.go b/coderd/database/modelqueries.go index dd25d8873986d..0810ef21b1493 100644 --- a/coderd/database/modelqueries.go +++ b/coderd/database/modelqueries.go @@ -836,6 +836,10 @@ func (q *sqlQuerier) GetAuthorizedChats(ctx context.Context, arg GetChatsParams, &i.Chat.GroupACL, &i.Chat.OwnerUsername, &i.Chat.OwnerName, + &i.Chat.ContextAggregateHash, + &i.Chat.ContextDirtySince, + &i.Chat.ContextDirtyResources, + &i.Chat.ContextError, &i.HasUnread); err != nil { return nil, err } @@ -910,7 +914,11 @@ func (q *sqlQuerier) GetAuthorizedChatsByChatFileID(ctx context.Context, fileID &i.UserACL, &i.GroupACL, &i.OwnerUsername, - &i.OwnerName); err != nil { + &i.OwnerName, + &i.ContextAggregateHash, + &i.ContextDirtySince, + &i.ContextDirtyResources, + &i.ContextError); err != nil { return nil, err } items = append(items, i) diff --git a/coderd/database/models.go b/coderd/database/models.go index b6e612081d652..f3069f9994e25 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -4797,6 +4797,10 @@ type Chat struct { GroupACL ChatACL `db:"group_acl" json:"group_acl"` OwnerUsername string `db:"owner_username" json:"owner_username"` OwnerName string `db:"owner_name" json:"owner_name"` + ContextAggregateHash []byte `db:"context_aggregate_hash" json:"context_aggregate_hash"` + ContextDirtySince sql.NullTime `db:"context_dirty_since" json:"context_dirty_since"` + ContextDirtyResources pqtype.NullRawMessage `db:"context_dirty_resources" json:"context_dirty_resources"` + ContextError string `db:"context_error" json:"context_error"` } type ChatDebugRun struct { @@ -4983,6 +4987,14 @@ type ChatTable struct { RetryStateVersion int64 `db:"retry_state_version" json:"retry_state_version"` RunnerID uuid.NullUUID `db:"runner_id" json:"runner_id"` RequiresActionDeadlineAt sql.NullTime `db:"requires_action_deadline_at" json:"requires_action_deadline_at"` + // Aggregate hash of the agent context snapshot this chat is pinned to. NULL until first hydrated; compared against the agent's latest snapshot hash to detect drift. + ContextAggregateHash []byte `db:"context_aggregate_hash" json:"context_aggregate_hash"` + // Set when an agent push changes the pinned hash; cleared on refresh. NULL means clean. + ContextDirtySince sql.NullTime `db:"context_dirty_since" json:"context_dirty_since"` + // Deterministic prefix of resources that changed since the pinned hash. Reserved for the dirty diff; left NULL until the UI phase populates it. + ContextDirtyResources pqtype.NullRawMessage `db:"context_dirty_resources" json:"context_dirty_resources"` + // Snapshot-level error copied from the pinned snapshot (count cap exceeded, watcher degraded, etc.). Empty when healthy. + ContextError string `db:"context_error" json:"context_error"` } type ChatUsageLimitConfig struct { diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 1f4f346846a63..c0401c1889b1b 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -994,6 +994,11 @@ type sqlcQuerier interface { GetWorkspacesByTemplateID(ctx context.Context, templateID uuid.UUID) ([]WorkspaceTable, error) GetWorkspacesEligibleForTransition(ctx context.Context, now time.Time) ([]GetWorkspacesEligibleForTransitionRow, error) GetWorkspacesForWorkspaceMetrics(ctx context.Context) ([]GetWorkspacesForWorkspaceMetricsRow, error) + // Stamps the pinned hash and error on every not-yet-hydrated chat for + // an agent (context_aggregate_hash IS NULL). Runs as a side effect of + // an agent push so chats created before the agent was ready pick up the + // snapshot without a dirty event. Does not bump updated_at. + HydrateAgentChatsContext(ctx context.Context, arg HydrateAgentChatsContextParams) error // Increments generation_attempt and returns the resulting value. IncrementChatGenerationAttempt(ctx context.Context, id uuid.UUID) (int64, error) InsertAIBridgeInterception(ctx context.Context, arg InsertAIBridgeInterceptionParams) (AIBridgeInterception, error) @@ -1175,6 +1180,12 @@ type sqlcQuerier interface { // allocate a new snapshot version in one round trip. LockChatAndBumpSnapshotVersion(ctx context.Context, id uuid.UUID) (Chat, error) MarkAllInboxNotificationsAsRead(ctx context.Context, arg MarkAllInboxNotificationsAsReadParams) error + // Flips active, already-hydrated chats for an agent to dirty when the + // agent's latest snapshot hash differs from the chat's pinned hash. The + // pinned hash is intentionally left untouched; the refresh endpoint + // re-pins it. Returns the chats that transitioned so the caller can + // emit watch events after the transaction commits. + MarkChatsContextDirtyByAgent(ctx context.Context, arg MarkChatsContextDirtyByAgentParams) ([]MarkChatsContextDirtyByAgentRow, error) OIDCClaimFieldValues(ctx context.Context, arg OIDCClaimFieldValuesParams) ([]string, error) // OIDCClaimFields returns a list of distinct keys in the the merged_claims fields. // This query is used to generate the list of available sync fields for idp sync settings. @@ -1219,6 +1230,11 @@ type sqlcQuerier interface { // for the table. // The CTE and the reorder is required because UPDATE doesn't guarantee order. SelectUsageEventsForPublishing(ctx context.Context, now time.Time) ([]UsageEvent, error) + // Pins a single chat to the supplied context snapshot hash and error + // and clears any dirty marker. Used by chat-create hydration and the + // refresh endpoint. Does not bump updated_at: context pinning is + // background state and must not reorder chat lists. + SetChatContextSnapshot(ctx context.Context, arg SetChatContextSnapshotParams) error SoftDeleteChatMessageByID(ctx context.Context, id int64) error SoftDeleteChatMessagesAfterID(ctx context.Context, arg SoftDeleteChatMessagesAfterIDParams) error SoftDeleteContextFileMessages(ctx context.Context, chatID uuid.UUID) error diff --git a/coderd/database/querier_test.go b/coderd/database/querier_test.go index 9d6ef0853c47e..9efdd91dcb0bb 100644 --- a/coderd/database/querier_test.go +++ b/coderd/database/querier_test.go @@ -1235,6 +1235,122 @@ func TestGetAuthorizedWorkspacesAndAgentsByOwnerID(t *testing.T) { }) } +func TestChatContextHydration(t *testing.T) { + t.Parallel() + if testing.Short() { + t.SkipNow() + } + + sqlDB := testSQLDB(t) + require.NoError(t, migrations.Up(sqlDB)) + db := database.New(sqlDB) + ctx := testutil.Context(t, testutil.WaitMedium) + + org := dbgen.Organization(t, db, database.Organization{}) + owner := dbgen.User(t, db, database.User{}) + _ = dbgen.ChatProvider(t, db, database.ChatProvider{Provider: "openai", DisplayName: "OpenAI"}) + modelCfg := dbgen.ChatModelConfig(t, db, database.ChatModelConfig{ + Provider: "openai", + Model: "test-model", + CreatedBy: uuid.NullUUID{UUID: owner.ID, Valid: true}, + UpdatedBy: uuid.NullUUID{UUID: owner.ID, Valid: true}, + IsDefault: true, + CompressionThreshold: 80, + }) + + // Chats are scoped per agent, so build two independent agents. + newAgent := func() database.WorkspaceAgent { + job := dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{OrganizationID: org.ID}) + resource := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{JobID: job.ID}) + return dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{ResourceID: resource.ID}) + } + agent := newAgent() + otherAgent := newAgent() + + newChat := func(status database.ChatStatus, agentID uuid.UUID) database.Chat { + return dbgen.Chat(t, db, database.Chat{ + OrganizationID: org.ID, + OwnerID: owner.ID, + LastModelConfigID: modelCfg.ID, + AgentID: uuid.NullUUID{UUID: agentID, Valid: true}, + Status: status, + }) + } + + hashH := []byte{0x01, 0x02, 0x03} + hashOther := []byte{0xff, 0xee} + + chatNull := newChat(database.ChatStatusWaiting, agent.ID) // never hydrated + chatMatch := newChat(database.ChatStatusRunning, agent.ID) // already at hashH + chatDrift := newChat(database.ChatStatusRunning, agent.ID) // drifted, active + chatTerminal := newChat(database.ChatStatusCompleted, agent.ID) // drifted, terminal + chatArchived := newChat(database.ChatStatusRunning, agent.ID) // drifted, archived + chatOtherAgent := newChat(database.ChatStatusRunning, otherAgent.ID) + + // Pin starting hashes; chatNull is intentionally left NULL. + require.NoError(t, db.SetChatContextSnapshot(ctx, database.SetChatContextSnapshotParams{ID: chatMatch.ID, AggregateHash: hashH})) + for _, id := range []uuid.UUID{chatDrift.ID, chatTerminal.ID, chatArchived.ID, chatOtherAgent.ID} { + require.NoError(t, db.SetChatContextSnapshot(ctx, database.SetChatContextSnapshotParams{ID: id, AggregateHash: hashOther})) + } + _, err := db.ArchiveChatByID(ctx, chatArchived.ID) + require.NoError(t, err) + + // Hydrate stamps only the NULL-hash chat for this agent. + require.NoError(t, db.HydrateAgentChatsContext(ctx, database.HydrateAgentChatsContextParams{ + AgentID: agent.ID, + AggregateHash: hashH, + })) + gotNull, err := db.GetChatByID(ctx, chatNull.ID) + require.NoError(t, err) + require.Equal(t, hashH, gotNull.ContextAggregateHash, "NULL-hash chat is hydrated") + gotDrift, err := db.GetChatByID(ctx, chatDrift.ID) + require.NoError(t, err) + require.Equal(t, hashOther, gotDrift.ContextAggregateHash, "hydrate must not overwrite an already-pinned hash") + + // Mark dirty: only the active, pinned, drifted chat for THIS agent flips. + // chatNull (now matches), chatMatch (matches), chatTerminal (status + // excluded), chatArchived (archived), and chatOtherAgent (other agent) + // are all left clean. + now := dbtime.Now() + flipped, err := db.MarkChatsContextDirtyByAgent(ctx, database.MarkChatsContextDirtyByAgentParams{ + AgentID: agent.ID, + AggregateHash: hashH, + DirtySince: sql.NullTime{Time: now, Valid: true}, + }) + require.NoError(t, err) + flippedIDs := make([]uuid.UUID, 0, len(flipped)) + for _, f := range flipped { + flippedIDs = append(flippedIDs, f.ID) + } + require.ElementsMatch(t, []uuid.UUID{chatDrift.ID}, flippedIDs) + + gotDrift, err = db.GetChatByID(ctx, chatDrift.ID) + require.NoError(t, err) + require.True(t, gotDrift.ContextDirtySince.Valid, "drifted chat is marked dirty") + + // Refresh re-pins to the latest hash and clears the dirty marker. + require.NoError(t, db.SetChatContextSnapshot(ctx, database.SetChatContextSnapshotParams{ID: chatDrift.ID, AggregateHash: hashH})) + gotDrift, err = db.GetChatByID(ctx, chatDrift.ID) + require.NoError(t, err) + require.Equal(t, hashH, gotDrift.ContextAggregateHash) + require.False(t, gotDrift.ContextDirtySince.Valid, "refresh clears the dirty marker") + + // With every chat now matching, a second mark is a no-op. + flipped, err = db.MarkChatsContextDirtyByAgent(ctx, database.MarkChatsContextDirtyByAgentParams{ + AgentID: agent.ID, + AggregateHash: hashH, + DirtySince: sql.NullTime{Time: now, Valid: true}, + }) + require.NoError(t, err) + require.Empty(t, flipped) + + // The other agent's chat is never touched by this agent's push. + gotOther, err := db.GetChatByID(ctx, chatOtherAgent.ID) + require.NoError(t, err) + require.Equal(t, hashOther, gotOther.ContextAggregateHash) + require.False(t, gotOther.ContextDirtySince.Valid) +} + func TestGetAuthorizedChats(t *testing.T) { t.Parallel() if testing.Short() { diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 5046f3c9a3a1b..fd2ce874261d1 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -5851,7 +5851,7 @@ WHERE LIMIT $3::int ) -RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at +RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error ), chats_expanded AS ( SELECT @@ -5894,13 +5894,17 @@ chats_expanded AS ( COALESCE(root.user_acl, acquired_chats.user_acl) AS user_acl, COALESCE(root.group_acl, acquired_chats.group_acl) AS group_acl, owner.username AS owner_username, - owner.name AS owner_name + owner.name AS owner_name, + acquired_chats.context_aggregate_hash, + acquired_chats.context_dirty_since, + acquired_chats.context_dirty_resources, + acquired_chats.context_error FROM acquired_chats LEFT JOIN chats root ON root.id = COALESCE(acquired_chats.root_chat_id, acquired_chats.parent_chat_id) JOIN visible_users owner ON owner.id = acquired_chats.owner_id ) -SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name +SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error FROM chats_expanded ` @@ -5962,6 +5966,10 @@ func (q *sqlQuerier) AcquireChats(ctx context.Context, arg AcquireChatsParams) ( &i.GroupACL, &i.OwnerUsername, &i.OwnerName, + &i.ContextAggregateHash, + &i.ContextDirtySince, + &i.ContextDirtyResources, + &i.ContextError, ); err != nil { return nil, err } @@ -6100,7 +6108,7 @@ WITH updated_chats AS ( UPDATE chats SET archived = true, pin_order = 0, updated_at = NOW() WHERE id = $1::uuid OR root_chat_id = $1::uuid - RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at + RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error ), chats_expanded AS ( SELECT @@ -6143,13 +6151,17 @@ chats_expanded AS ( COALESCE(root.user_acl, updated_chats.user_acl) AS user_acl, COALESCE(root.group_acl, updated_chats.group_acl) AS group_acl, owner.username AS owner_username, - owner.name AS owner_name + owner.name AS owner_name, + updated_chats.context_aggregate_hash, + updated_chats.context_dirty_since, + updated_chats.context_dirty_resources, + updated_chats.context_error FROM updated_chats LEFT JOIN chats root ON root.id = COALESCE(updated_chats.root_chat_id, updated_chats.parent_chat_id) JOIN visible_users owner ON owner.id = updated_chats.owner_id ) -SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name +SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error FROM chats_expanded ORDER BY (chats_expanded.id = $1::uuid) DESC, chats_expanded.created_at ASC, chats_expanded.id ASC ` @@ -6204,6 +6216,10 @@ func (q *sqlQuerier) ArchiveChatByID(ctx context.Context, id uuid.UUID) ([]Chat, &i.GroupACL, &i.OwnerUsername, &i.OwnerName, + &i.ContextAggregateHash, + &i.ContextDirtySince, + &i.ContextDirtyResources, + &i.ContextError, ); err != nil { return nil, err } @@ -6254,10 +6270,10 @@ archived AS ( FROM to_archive t WHERE (c.id = t.id OR c.root_chat_id = t.id) -- cascade to children AND c.archived = false - RETURNING c.id, c.owner_id, c.workspace_id, c.title, c.status, c.worker_id, c.started_at, c.heartbeat_at, c.created_at, c.updated_at, c.parent_chat_id, c.root_chat_id, c.last_model_config_id, c.archived, c.last_error, c.mode, c.mcp_server_ids, c.labels, c.build_id, c.agent_id, c.pin_order, c.last_read_message_id, c.last_injected_context, c.dynamic_tools, c.organization_id, c.plan_mode, c.client_type, c.last_turn_summary, c.user_acl, c.group_acl, c.snapshot_version, c.history_version, c.queue_version, c.generation_attempt, c.retry_state, c.retry_state_version, c.runner_id, c.requires_action_deadline_at + RETURNING c.id, c.owner_id, c.workspace_id, c.title, c.status, c.worker_id, c.started_at, c.heartbeat_at, c.created_at, c.updated_at, c.parent_chat_id, c.root_chat_id, c.last_model_config_id, c.archived, c.last_error, c.mode, c.mcp_server_ids, c.labels, c.build_id, c.agent_id, c.pin_order, c.last_read_message_id, c.last_injected_context, c.dynamic_tools, c.organization_id, c.plan_mode, c.client_type, c.last_turn_summary, c.user_acl, c.group_acl, c.snapshot_version, c.history_version, c.queue_version, c.generation_attempt, c.retry_state, c.retry_state_version, c.runner_id, c.requires_action_deadline_at, c.context_aggregate_hash, c.context_dirty_since, c.context_dirty_resources, c.context_error ) SELECT - a.id, a.owner_id, a.workspace_id, a.title, a.status, a.worker_id, a.started_at, a.heartbeat_at, a.created_at, a.updated_at, a.parent_chat_id, a.root_chat_id, a.last_model_config_id, a.archived, a.last_error, a.mode, a.mcp_server_ids, a.labels, a.build_id, a.agent_id, a.pin_order, a.last_read_message_id, a.last_injected_context, a.dynamic_tools, a.organization_id, a.plan_mode, a.client_type, a.last_turn_summary, a.user_acl, a.group_acl, a.snapshot_version, a.history_version, a.queue_version, a.generation_attempt, a.retry_state, a.retry_state_version, a.runner_id, a.requires_action_deadline_at, + a.id, a.owner_id, a.workspace_id, a.title, a.status, a.worker_id, a.started_at, a.heartbeat_at, a.created_at, a.updated_at, a.parent_chat_id, a.root_chat_id, a.last_model_config_id, a.archived, a.last_error, a.mode, a.mcp_server_ids, a.labels, a.build_id, a.agent_id, a.pin_order, a.last_read_message_id, a.last_injected_context, a.dynamic_tools, a.organization_id, a.plan_mode, a.client_type, a.last_turn_summary, a.user_acl, a.group_acl, a.snapshot_version, a.history_version, a.queue_version, a.generation_attempt, a.retry_state, a.retry_state_version, a.runner_id, a.requires_action_deadline_at, a.context_aggregate_hash, a.context_dirty_since, a.context_dirty_resources, a.context_error, -- Children inherit their root's activity so last_activity_at is never null. COALESCE( t.last_activity_at, @@ -6313,6 +6329,10 @@ type AutoArchiveInactiveChatsRow struct { RetryStateVersion int64 `db:"retry_state_version" json:"retry_state_version"` RunnerID uuid.NullUUID `db:"runner_id" json:"runner_id"` RequiresActionDeadlineAt sql.NullTime `db:"requires_action_deadline_at" json:"requires_action_deadline_at"` + ContextAggregateHash []byte `db:"context_aggregate_hash" json:"context_aggregate_hash"` + ContextDirtySince sql.NullTime `db:"context_dirty_since" json:"context_dirty_since"` + ContextDirtyResources pqtype.NullRawMessage `db:"context_dirty_resources" json:"context_dirty_resources"` + ContextError string `db:"context_error" json:"context_error"` LastActivityAt time.Time `db:"last_activity_at" json:"last_activity_at"` } @@ -6371,6 +6391,10 @@ func (q *sqlQuerier) AutoArchiveInactiveChats(ctx context.Context, arg AutoArchi &i.RetryStateVersion, &i.RunnerID, &i.RequiresActionDeadlineAt, + &i.ContextAggregateHash, + &i.ContextDirtySince, + &i.ContextDirtyResources, + &i.ContextError, &i.LastActivityAt, ); err != nil { return nil, err @@ -6635,7 +6659,7 @@ func (q *sqlQuerier) DeleteStaleChatHeartbeats(ctx context.Context, staleSeconds } const getActiveChatsByAgentID = `-- name: GetActiveChatsByAgentID :many -SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name +SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error FROM chats_expanded WHERE agent_id = $1::uuid AND archived = false @@ -6696,6 +6720,10 @@ func (q *sqlQuerier) GetActiveChatsByAgentID(ctx context.Context, agentID uuid.U &i.GroupACL, &i.OwnerUsername, &i.OwnerName, + &i.ContextAggregateHash, + &i.ContextDirtySince, + &i.ContextDirtyResources, + &i.ContextError, ); err != nil { return nil, err } @@ -6712,7 +6740,7 @@ func (q *sqlQuerier) GetActiveChatsByAgentID(ctx context.Context, agentID uuid.U const getAutoArchiveInactiveChatCandidates = `-- name: GetAutoArchiveInactiveChatCandidates :many SELECT - chats_expanded.id, chats_expanded.owner_id, chats_expanded.workspace_id, chats_expanded.title, chats_expanded.status, chats_expanded.worker_id, chats_expanded.started_at, chats_expanded.heartbeat_at, chats_expanded.created_at, chats_expanded.updated_at, chats_expanded.parent_chat_id, chats_expanded.root_chat_id, chats_expanded.last_model_config_id, chats_expanded.archived, chats_expanded.last_error, chats_expanded.mode, chats_expanded.mcp_server_ids, chats_expanded.labels, chats_expanded.build_id, chats_expanded.agent_id, chats_expanded.pin_order, chats_expanded.last_read_message_id, chats_expanded.last_injected_context, chats_expanded.dynamic_tools, chats_expanded.organization_id, chats_expanded.plan_mode, chats_expanded.client_type, chats_expanded.last_turn_summary, chats_expanded.snapshot_version, chats_expanded.history_version, chats_expanded.queue_version, chats_expanded.generation_attempt, chats_expanded.retry_state, chats_expanded.retry_state_version, chats_expanded.runner_id, chats_expanded.requires_action_deadline_at, chats_expanded.user_acl, chats_expanded.group_acl, chats_expanded.owner_username, chats_expanded.owner_name, + chats_expanded.id, chats_expanded.owner_id, chats_expanded.workspace_id, chats_expanded.title, chats_expanded.status, chats_expanded.worker_id, chats_expanded.started_at, chats_expanded.heartbeat_at, chats_expanded.created_at, chats_expanded.updated_at, chats_expanded.parent_chat_id, chats_expanded.root_chat_id, chats_expanded.last_model_config_id, chats_expanded.archived, chats_expanded.last_error, chats_expanded.mode, chats_expanded.mcp_server_ids, chats_expanded.labels, chats_expanded.build_id, chats_expanded.agent_id, chats_expanded.pin_order, chats_expanded.last_read_message_id, chats_expanded.last_injected_context, chats_expanded.dynamic_tools, chats_expanded.organization_id, chats_expanded.plan_mode, chats_expanded.client_type, chats_expanded.last_turn_summary, chats_expanded.snapshot_version, chats_expanded.history_version, chats_expanded.queue_version, chats_expanded.generation_attempt, chats_expanded.retry_state, chats_expanded.retry_state_version, chats_expanded.runner_id, chats_expanded.requires_action_deadline_at, chats_expanded.user_acl, chats_expanded.group_acl, chats_expanded.owner_username, chats_expanded.owner_name, chats_expanded.context_aggregate_hash, chats_expanded.context_dirty_since, chats_expanded.context_dirty_resources, chats_expanded.context_error, COALESCE(activity.last_activity_at, chats_expanded.created_at)::timestamptz AS last_activity_at FROM chats_expanded LEFT JOIN LATERAL ( @@ -6785,6 +6813,10 @@ type GetAutoArchiveInactiveChatCandidatesRow struct { GroupACL ChatACL `db:"group_acl" json:"group_acl"` OwnerUsername string `db:"owner_username" json:"owner_username"` OwnerName string `db:"owner_name" json:"owner_name"` + ContextAggregateHash []byte `db:"context_aggregate_hash" json:"context_aggregate_hash"` + ContextDirtySince sql.NullTime `db:"context_dirty_since" json:"context_dirty_since"` + ContextDirtyResources pqtype.NullRawMessage `db:"context_dirty_resources" json:"context_dirty_resources"` + ContextError string `db:"context_error" json:"context_error"` LastActivityAt time.Time `db:"last_activity_at" json:"last_activity_at"` } @@ -6841,6 +6873,10 @@ func (q *sqlQuerier) GetAutoArchiveInactiveChatCandidates(ctx context.Context, a &i.GroupACL, &i.OwnerUsername, &i.OwnerName, + &i.ContextAggregateHash, + &i.ContextDirtySince, + &i.ContextDirtyResources, + &i.ContextError, &i.LastActivityAt, ); err != nil { return nil, err @@ -6879,7 +6915,7 @@ func (q *sqlQuerier) GetChatACLByID(ctx context.Context, id uuid.UUID) (GetChatA } const getChatByID = `-- name: GetChatByID :one -SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name +SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error FROM chats_expanded WHERE id = $1::uuid ` @@ -6928,13 +6964,17 @@ func (q *sqlQuerier) GetChatByID(ctx context.Context, id uuid.UUID) (Chat, error &i.GroupACL, &i.OwnerUsername, &i.OwnerName, + &i.ContextAggregateHash, + &i.ContextDirtySince, + &i.ContextDirtyResources, + &i.ContextError, ) return i, err } const getChatByIDForShare = `-- name: GetChatByIDForShare :one WITH shared_chat AS ( - SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at + SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error FROM chats WHERE id = $1::uuid FOR SHARE @@ -6980,13 +7020,17 @@ chats_expanded AS ( COALESCE(root.user_acl, shared_chat.user_acl) AS user_acl, COALESCE(root.group_acl, shared_chat.group_acl) AS group_acl, owner.username AS owner_username, - owner.name AS owner_name + owner.name AS owner_name, + shared_chat.context_aggregate_hash, + shared_chat.context_dirty_since, + shared_chat.context_dirty_resources, + shared_chat.context_error FROM shared_chat LEFT JOIN chats root ON root.id = COALESCE(shared_chat.root_chat_id, shared_chat.parent_chat_id) JOIN visible_users owner ON owner.id = shared_chat.owner_id ) -SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name +SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error FROM chats_expanded ` @@ -7034,13 +7078,17 @@ func (q *sqlQuerier) GetChatByIDForShare(ctx context.Context, id uuid.UUID) (Cha &i.GroupACL, &i.OwnerUsername, &i.OwnerName, + &i.ContextAggregateHash, + &i.ContextDirtySince, + &i.ContextDirtyResources, + &i.ContextError, ) return i, err } const getChatByIDForUpdate = `-- name: GetChatByIDForUpdate :one WITH locked_chat AS ( - SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at + SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error FROM chats WHERE id = $1::uuid FOR UPDATE @@ -7086,13 +7134,17 @@ chats_expanded AS ( COALESCE(root.user_acl, locked_chat.user_acl) AS user_acl, COALESCE(root.group_acl, locked_chat.group_acl) AS group_acl, owner.username AS owner_username, - owner.name AS owner_name + owner.name AS owner_name, + locked_chat.context_aggregate_hash, + locked_chat.context_dirty_since, + locked_chat.context_dirty_resources, + locked_chat.context_error FROM locked_chat LEFT JOIN chats root ON root.id = COALESCE(locked_chat.root_chat_id, locked_chat.parent_chat_id) JOIN visible_users owner ON owner.id = locked_chat.owner_id ) -SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name +SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error FROM chats_expanded ` @@ -7140,6 +7192,10 @@ func (q *sqlQuerier) GetChatByIDForUpdate(ctx context.Context, id uuid.UUID) (Ch &i.GroupACL, &i.OwnerUsername, &i.OwnerName, + &i.ContextAggregateHash, + &i.ContextDirtySince, + &i.ContextDirtyResources, + &i.ContextError, ) return i, err } @@ -8588,7 +8644,7 @@ func (q *sqlQuerier) GetChatUserPromptsByChatID(ctx context.Context, arg GetChat const getChatWorkerAcquisitionCandidates = `-- name: GetChatWorkerAcquisitionCandidates :many SELECT - chats_expanded.id, chats_expanded.owner_id, chats_expanded.workspace_id, chats_expanded.title, chats_expanded.status, chats_expanded.worker_id, chats_expanded.started_at, chats_expanded.heartbeat_at, chats_expanded.created_at, chats_expanded.updated_at, chats_expanded.parent_chat_id, chats_expanded.root_chat_id, chats_expanded.last_model_config_id, chats_expanded.archived, chats_expanded.last_error, chats_expanded.mode, chats_expanded.mcp_server_ids, chats_expanded.labels, chats_expanded.build_id, chats_expanded.agent_id, chats_expanded.pin_order, chats_expanded.last_read_message_id, chats_expanded.last_injected_context, chats_expanded.dynamic_tools, chats_expanded.organization_id, chats_expanded.plan_mode, chats_expanded.client_type, chats_expanded.last_turn_summary, chats_expanded.snapshot_version, chats_expanded.history_version, chats_expanded.queue_version, chats_expanded.generation_attempt, chats_expanded.retry_state, chats_expanded.retry_state_version, chats_expanded.runner_id, chats_expanded.requires_action_deadline_at, chats_expanded.user_acl, chats_expanded.group_acl, chats_expanded.owner_username, chats_expanded.owner_name, + chats_expanded.id, chats_expanded.owner_id, chats_expanded.workspace_id, chats_expanded.title, chats_expanded.status, chats_expanded.worker_id, chats_expanded.started_at, chats_expanded.heartbeat_at, chats_expanded.created_at, chats_expanded.updated_at, chats_expanded.parent_chat_id, chats_expanded.root_chat_id, chats_expanded.last_model_config_id, chats_expanded.archived, chats_expanded.last_error, chats_expanded.mode, chats_expanded.mcp_server_ids, chats_expanded.labels, chats_expanded.build_id, chats_expanded.agent_id, chats_expanded.pin_order, chats_expanded.last_read_message_id, chats_expanded.last_injected_context, chats_expanded.dynamic_tools, chats_expanded.organization_id, chats_expanded.plan_mode, chats_expanded.client_type, chats_expanded.last_turn_summary, chats_expanded.snapshot_version, chats_expanded.history_version, chats_expanded.queue_version, chats_expanded.generation_attempt, chats_expanded.retry_state, chats_expanded.retry_state_version, chats_expanded.runner_id, chats_expanded.requires_action_deadline_at, chats_expanded.user_acl, chats_expanded.group_acl, chats_expanded.owner_username, chats_expanded.owner_name, chats_expanded.context_aggregate_hash, chats_expanded.context_dirty_since, chats_expanded.context_dirty_resources, chats_expanded.context_error, chat_heartbeats.heartbeat_at AS current_heartbeat_at, NOT EXISTS ( SELECT 1 @@ -8665,6 +8721,10 @@ type GetChatWorkerAcquisitionCandidatesRow struct { GroupACL ChatACL `db:"group_acl" json:"group_acl"` OwnerUsername string `db:"owner_username" json:"owner_username"` OwnerName string `db:"owner_name" json:"owner_name"` + ContextAggregateHash []byte `db:"context_aggregate_hash" json:"context_aggregate_hash"` + ContextDirtySince sql.NullTime `db:"context_dirty_since" json:"context_dirty_since"` + ContextDirtyResources pqtype.NullRawMessage `db:"context_dirty_resources" json:"context_dirty_resources"` + ContextError string `db:"context_error" json:"context_error"` CurrentHeartbeatAt sql.NullTime `db:"current_heartbeat_at" json:"current_heartbeat_at"` HeartbeatStale bool `db:"heartbeat_stale" json:"heartbeat_stale"` } @@ -8730,6 +8790,10 @@ func (q *sqlQuerier) GetChatWorkerAcquisitionCandidates(ctx context.Context, arg &i.GroupACL, &i.OwnerUsername, &i.OwnerName, + &i.ContextAggregateHash, + &i.ContextDirtySince, + &i.ContextDirtyResources, + &i.ContextError, &i.CurrentHeartbeatAt, &i.HeartbeatStale, ); err != nil { @@ -8756,7 +8820,7 @@ WITH cursor_chat AS ( WHERE id = $7 ) SELECT - chats_expanded.id, chats_expanded.owner_id, chats_expanded.workspace_id, chats_expanded.title, chats_expanded.status, chats_expanded.worker_id, chats_expanded.started_at, chats_expanded.heartbeat_at, chats_expanded.created_at, chats_expanded.updated_at, chats_expanded.parent_chat_id, chats_expanded.root_chat_id, chats_expanded.last_model_config_id, chats_expanded.archived, chats_expanded.last_error, chats_expanded.mode, chats_expanded.mcp_server_ids, chats_expanded.labels, chats_expanded.build_id, chats_expanded.agent_id, chats_expanded.pin_order, chats_expanded.last_read_message_id, chats_expanded.last_injected_context, chats_expanded.dynamic_tools, chats_expanded.organization_id, chats_expanded.plan_mode, chats_expanded.client_type, chats_expanded.last_turn_summary, chats_expanded.snapshot_version, chats_expanded.history_version, chats_expanded.queue_version, chats_expanded.generation_attempt, chats_expanded.retry_state, chats_expanded.retry_state_version, chats_expanded.runner_id, chats_expanded.requires_action_deadline_at, chats_expanded.user_acl, chats_expanded.group_acl, chats_expanded.owner_username, chats_expanded.owner_name, + chats_expanded.id, chats_expanded.owner_id, chats_expanded.workspace_id, chats_expanded.title, chats_expanded.status, chats_expanded.worker_id, chats_expanded.started_at, chats_expanded.heartbeat_at, chats_expanded.created_at, chats_expanded.updated_at, chats_expanded.parent_chat_id, chats_expanded.root_chat_id, chats_expanded.last_model_config_id, chats_expanded.archived, chats_expanded.last_error, chats_expanded.mode, chats_expanded.mcp_server_ids, chats_expanded.labels, chats_expanded.build_id, chats_expanded.agent_id, chats_expanded.pin_order, chats_expanded.last_read_message_id, chats_expanded.last_injected_context, chats_expanded.dynamic_tools, chats_expanded.organization_id, chats_expanded.plan_mode, chats_expanded.client_type, chats_expanded.last_turn_summary, chats_expanded.snapshot_version, chats_expanded.history_version, chats_expanded.queue_version, chats_expanded.generation_attempt, chats_expanded.retry_state, chats_expanded.retry_state_version, chats_expanded.runner_id, chats_expanded.requires_action_deadline_at, chats_expanded.user_acl, chats_expanded.group_acl, chats_expanded.owner_username, chats_expanded.owner_name, chats_expanded.context_aggregate_hash, chats_expanded.context_dirty_since, chats_expanded.context_dirty_resources, chats_expanded.context_error, EXISTS ( SELECT 1 FROM chat_messages cm WHERE cm.chat_id = chats_expanded.id @@ -9008,6 +9072,10 @@ func (q *sqlQuerier) GetChats(ctx context.Context, arg GetChatsParams) ([]GetCha &i.Chat.GroupACL, &i.Chat.OwnerUsername, &i.Chat.OwnerName, + &i.Chat.ContextAggregateHash, + &i.Chat.ContextDirtySince, + &i.Chat.ContextDirtyResources, + &i.Chat.ContextError, &i.HasUnread, ); err != nil { return nil, err @@ -9025,7 +9093,7 @@ func (q *sqlQuerier) GetChats(ctx context.Context, arg GetChatsParams) ([]GetCha const getChatsByChatFileID = `-- name: GetChatsByChatFileID :many SELECT - id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name + id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error FROM chats_expanded WHERE @@ -9088,6 +9156,10 @@ func (q *sqlQuerier) GetChatsByChatFileID(ctx context.Context, fileID uuid.UUID) &i.GroupACL, &i.OwnerUsername, &i.OwnerName, + &i.ContextAggregateHash, + &i.ContextDirtySince, + &i.ContextDirtyResources, + &i.ContextError, ); err != nil { return nil, err } @@ -9103,7 +9175,7 @@ func (q *sqlQuerier) GetChatsByChatFileID(ctx context.Context, fileID uuid.UUID) } const getChatsByIDsForRunnerSync = `-- name: GetChatsByIDsForRunnerSync :many -SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name +SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error FROM chats_expanded WHERE id = ANY($1::uuid[]) ORDER BY id ASC @@ -9159,6 +9231,10 @@ func (q *sqlQuerier) GetChatsByIDsForRunnerSync(ctx context.Context, ids []uuid. &i.GroupACL, &i.OwnerUsername, &i.OwnerName, + &i.ContextAggregateHash, + &i.ContextDirtySince, + &i.ContextDirtyResources, + &i.ContextError, ); err != nil { return nil, err } @@ -9174,7 +9250,7 @@ func (q *sqlQuerier) GetChatsByIDsForRunnerSync(ctx context.Context, ids []uuid. } const getChatsByWorkspaceIDs = `-- name: GetChatsByWorkspaceIDs :many -SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name +SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error FROM chats_expanded WHERE archived = false AND workspace_id = ANY($1::uuid[]) @@ -9231,6 +9307,10 @@ func (q *sqlQuerier) GetChatsByWorkspaceIDs(ctx context.Context, ids []uuid.UUID &i.GroupACL, &i.OwnerUsername, &i.OwnerName, + &i.ContextAggregateHash, + &i.ContextDirtySince, + &i.ContextDirtyResources, + &i.ContextError, ); err != nil { return nil, err } @@ -9315,7 +9395,7 @@ func (q *sqlQuerier) GetChatsUpdatedAfter(ctx context.Context, updatedAfter time const getChildChatsByParentIDs = `-- name: GetChildChatsByParentIDs :many SELECT - chats_expanded.id, chats_expanded.owner_id, chats_expanded.workspace_id, chats_expanded.title, chats_expanded.status, chats_expanded.worker_id, chats_expanded.started_at, chats_expanded.heartbeat_at, chats_expanded.created_at, chats_expanded.updated_at, chats_expanded.parent_chat_id, chats_expanded.root_chat_id, chats_expanded.last_model_config_id, chats_expanded.archived, chats_expanded.last_error, chats_expanded.mode, chats_expanded.mcp_server_ids, chats_expanded.labels, chats_expanded.build_id, chats_expanded.agent_id, chats_expanded.pin_order, chats_expanded.last_read_message_id, chats_expanded.last_injected_context, chats_expanded.dynamic_tools, chats_expanded.organization_id, chats_expanded.plan_mode, chats_expanded.client_type, chats_expanded.last_turn_summary, chats_expanded.snapshot_version, chats_expanded.history_version, chats_expanded.queue_version, chats_expanded.generation_attempt, chats_expanded.retry_state, chats_expanded.retry_state_version, chats_expanded.runner_id, chats_expanded.requires_action_deadline_at, chats_expanded.user_acl, chats_expanded.group_acl, chats_expanded.owner_username, chats_expanded.owner_name, + chats_expanded.id, chats_expanded.owner_id, chats_expanded.workspace_id, chats_expanded.title, chats_expanded.status, chats_expanded.worker_id, chats_expanded.started_at, chats_expanded.heartbeat_at, chats_expanded.created_at, chats_expanded.updated_at, chats_expanded.parent_chat_id, chats_expanded.root_chat_id, chats_expanded.last_model_config_id, chats_expanded.archived, chats_expanded.last_error, chats_expanded.mode, chats_expanded.mcp_server_ids, chats_expanded.labels, chats_expanded.build_id, chats_expanded.agent_id, chats_expanded.pin_order, chats_expanded.last_read_message_id, chats_expanded.last_injected_context, chats_expanded.dynamic_tools, chats_expanded.organization_id, chats_expanded.plan_mode, chats_expanded.client_type, chats_expanded.last_turn_summary, chats_expanded.snapshot_version, chats_expanded.history_version, chats_expanded.queue_version, chats_expanded.generation_attempt, chats_expanded.retry_state, chats_expanded.retry_state_version, chats_expanded.runner_id, chats_expanded.requires_action_deadline_at, chats_expanded.user_acl, chats_expanded.group_acl, chats_expanded.owner_username, chats_expanded.owner_name, chats_expanded.context_aggregate_hash, chats_expanded.context_dirty_since, chats_expanded.context_dirty_resources, chats_expanded.context_error, EXISTS ( SELECT 1 FROM chat_messages cm WHERE cm.chat_id = chats_expanded.id @@ -9400,6 +9480,10 @@ func (q *sqlQuerier) GetChildChatsByParentIDs(ctx context.Context, arg GetChildC &i.Chat.GroupACL, &i.Chat.OwnerUsername, &i.Chat.OwnerName, + &i.Chat.ContextAggregateHash, + &i.Chat.ContextDirtySince, + &i.Chat.ContextDirtyResources, + &i.Chat.ContextError, &i.HasUnread, ); err != nil { return nil, err @@ -9482,7 +9566,7 @@ func (q *sqlQuerier) GetLastChatMessageByRole(ctx context.Context, arg GetLastCh const getStaleChats = `-- name: GetStaleChats :many SELECT - id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name + id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error FROM chats_expanded WHERE @@ -9555,6 +9639,10 @@ func (q *sqlQuerier) GetStaleChats(ctx context.Context, staleThreshold time.Time &i.GroupACL, &i.OwnerUsername, &i.OwnerName, + &i.ContextAggregateHash, + &i.ContextDirtySince, + &i.ContextDirtyResources, + &i.ContextError, ); err != nil { return nil, err } @@ -9631,6 +9719,31 @@ func (q *sqlQuerier) GetUserGroupSpendLimit(ctx context.Context, arg GetUserGrou return limit_micros, err } +const hydrateAgentChatsContext = `-- name: HydrateAgentChatsContext :exec +UPDATE chats +SET + context_aggregate_hash = $1, + context_error = $2 +WHERE agent_id = $3::uuid + AND archived = false + AND context_aggregate_hash IS NULL +` + +type HydrateAgentChatsContextParams struct { + AggregateHash []byte `db:"aggregate_hash" json:"aggregate_hash"` + ContextError string `db:"context_error" json:"context_error"` + AgentID uuid.UUID `db:"agent_id" json:"agent_id"` +} + +// Stamps the pinned hash and error on every not-yet-hydrated chat for +// an agent (context_aggregate_hash IS NULL). Runs as a side effect of +// an agent push so chats created before the agent was ready pick up the +// snapshot without a dirty event. Does not bump updated_at. +func (q *sqlQuerier) HydrateAgentChatsContext(ctx context.Context, arg HydrateAgentChatsContextParams) error { + _, err := q.db.ExecContext(ctx, hydrateAgentChatsContext, arg.AggregateHash, arg.ContextError, arg.AgentID) + return err +} + const incrementChatGenerationAttempt = `-- name: IncrementChatGenerationAttempt :one UPDATE chats SET generation_attempt = generation_attempt + 1, updated_at = NOW() @@ -9683,7 +9796,7 @@ INSERT INTO chats ( $15::jsonb, $16::chat_client_type ) -RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at +RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error ), chats_expanded AS ( SELECT @@ -9726,13 +9839,17 @@ chats_expanded AS ( COALESCE(root.user_acl, inserted_chat.user_acl) AS user_acl, COALESCE(root.group_acl, inserted_chat.group_acl) AS group_acl, owner.username AS owner_username, - owner.name AS owner_name + owner.name AS owner_name, + inserted_chat.context_aggregate_hash, + inserted_chat.context_dirty_since, + inserted_chat.context_dirty_resources, + inserted_chat.context_error FROM inserted_chat LEFT JOIN chats root ON root.id = COALESCE(inserted_chat.root_chat_id, inserted_chat.parent_chat_id) JOIN visible_users owner ON owner.id = inserted_chat.owner_id ) -SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name +SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error FROM chats_expanded ` @@ -9816,6 +9933,10 @@ func (q *sqlQuerier) InsertChat(ctx context.Context, arg InsertChatParams) (Chat &i.GroupACL, &i.OwnerUsername, &i.OwnerName, + &i.ContextAggregateHash, + &i.ContextDirtySince, + &i.ContextDirtyResources, + &i.ContextError, ) return i, err } @@ -10258,7 +10379,7 @@ WITH bumped_chat AS ( WHERE id = $1::uuid FOR UPDATE ) - RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at + RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error ), chats_expanded AS ( SELECT @@ -10301,12 +10422,16 @@ chats_expanded AS ( COALESCE(root.user_acl, bumped_chat.user_acl) AS user_acl, COALESCE(root.group_acl, bumped_chat.group_acl) AS group_acl, owner.username AS owner_username, - owner.name AS owner_name + owner.name AS owner_name, + bumped_chat.context_aggregate_hash, + bumped_chat.context_dirty_since, + bumped_chat.context_dirty_resources, + bumped_chat.context_error FROM bumped_chat LEFT JOIN chats root ON root.id = COALESCE(bumped_chat.root_chat_id, bumped_chat.parent_chat_id) JOIN visible_users owner ON owner.id = bumped_chat.owner_id ) -SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name +SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error FROM chats_expanded ` @@ -10358,10 +10483,65 @@ func (q *sqlQuerier) LockChatAndBumpSnapshotVersion(ctx context.Context, id uuid &i.GroupACL, &i.OwnerUsername, &i.OwnerName, + &i.ContextAggregateHash, + &i.ContextDirtySince, + &i.ContextDirtyResources, + &i.ContextError, ) return i, err } +const markChatsContextDirtyByAgent = `-- name: MarkChatsContextDirtyByAgent :many +UPDATE chats +SET context_dirty_since = $1 +WHERE agent_id = $2::uuid + AND archived = false + AND status IN ('waiting', 'running', 'paused', 'pending', 'requires_action') + AND context_aggregate_hash IS NOT NULL + AND context_aggregate_hash IS DISTINCT FROM $3 + AND context_dirty_since IS NULL +RETURNING id, owner_id +` + +type MarkChatsContextDirtyByAgentParams struct { + DirtySince sql.NullTime `db:"dirty_since" json:"dirty_since"` + AgentID uuid.UUID `db:"agent_id" json:"agent_id"` + AggregateHash []byte `db:"aggregate_hash" json:"aggregate_hash"` +} + +type MarkChatsContextDirtyByAgentRow struct { + ID uuid.UUID `db:"id" json:"id"` + OwnerID uuid.UUID `db:"owner_id" json:"owner_id"` +} + +// Flips active, already-hydrated chats for an agent to dirty when the +// agent's latest snapshot hash differs from the chat's pinned hash. The +// pinned hash is intentionally left untouched; the refresh endpoint +// re-pins it. Returns the chats that transitioned so the caller can +// emit watch events after the transaction commits. +func (q *sqlQuerier) MarkChatsContextDirtyByAgent(ctx context.Context, arg MarkChatsContextDirtyByAgentParams) ([]MarkChatsContextDirtyByAgentRow, error) { + rows, err := q.db.QueryContext(ctx, markChatsContextDirtyByAgent, arg.DirtySince, arg.AgentID, arg.AggregateHash) + if err != nil { + return nil, err + } + defer rows.Close() + var items []MarkChatsContextDirtyByAgentRow + for rows.Next() { + var i MarkChatsContextDirtyByAgentRow + if err := rows.Scan(&i.ID, &i.OwnerID); 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 pinChatByID = `-- name: PinChatByID :exec WITH target_chat AS ( SELECT @@ -10561,6 +10741,30 @@ func (q *sqlQuerier) ResolveUserChatSpendLimit(ctx context.Context, arg ResolveU return i, err } +const setChatContextSnapshot = `-- name: SetChatContextSnapshot :exec +UPDATE chats +SET + context_aggregate_hash = $1, + context_error = $2, + context_dirty_since = NULL +WHERE id = $3::uuid +` + +type SetChatContextSnapshotParams struct { + AggregateHash []byte `db:"aggregate_hash" json:"aggregate_hash"` + ContextError string `db:"context_error" json:"context_error"` + ID uuid.UUID `db:"id" json:"id"` +} + +// Pins a single chat to the supplied context snapshot hash and error +// and clears any dirty marker. Used by chat-create hydration and the +// refresh endpoint. Does not bump updated_at: context pinning is +// background state and must not reorder chat lists. +func (q *sqlQuerier) SetChatContextSnapshot(ctx context.Context, arg SetChatContextSnapshotParams) error { + _, err := q.db.ExecContext(ctx, setChatContextSnapshot, arg.AggregateHash, arg.ContextError, arg.ID) + return err +} + const softDeleteChatMessageByID = `-- name: SoftDeleteChatMessageByID :exec UPDATE chat_messages @@ -10613,7 +10817,7 @@ WITH updated_chats AS ( archived = false, updated_at = NOW() WHERE id = $1::uuid OR root_chat_id = $1::uuid - RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at + RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error ), chats_expanded AS ( SELECT @@ -10656,13 +10860,17 @@ chats_expanded AS ( COALESCE(root.user_acl, updated_chats.user_acl) AS user_acl, COALESCE(root.group_acl, updated_chats.group_acl) AS group_acl, owner.username AS owner_username, - owner.name AS owner_name + owner.name AS owner_name, + updated_chats.context_aggregate_hash, + updated_chats.context_dirty_since, + updated_chats.context_dirty_resources, + updated_chats.context_error FROM updated_chats LEFT JOIN chats root ON root.id = COALESCE(updated_chats.root_chat_id, updated_chats.parent_chat_id) JOIN visible_users owner ON owner.id = updated_chats.owner_id ) -SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name +SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error FROM chats_expanded ORDER BY (chats_expanded.id = $1::uuid) DESC, chats_expanded.created_at ASC, chats_expanded.id ASC ` @@ -10721,6 +10929,10 @@ func (q *sqlQuerier) UnarchiveChatByID(ctx context.Context, id uuid.UUID) ([]Cha &i.GroupACL, &i.OwnerUsername, &i.OwnerName, + &i.ContextAggregateHash, + &i.ContextDirtySince, + &i.ContextDirtyResources, + &i.ContextError, ); err != nil { return nil, err } @@ -10823,7 +11035,7 @@ UPDATE chats SET updated_at = NOW() WHERE id = $3::uuid -RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at +RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error ), chats_expanded AS ( SELECT @@ -10866,13 +11078,17 @@ chats_expanded AS ( COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl, COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl, owner.username AS owner_username, - owner.name AS owner_name + owner.name AS owner_name, + updated_chat.context_aggregate_hash, + updated_chat.context_dirty_since, + updated_chat.context_dirty_resources, + updated_chat.context_error FROM updated_chat LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id) JOIN visible_users owner ON owner.id = updated_chat.owner_id ) -SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name +SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error FROM chats_expanded ` @@ -10926,6 +11142,10 @@ func (q *sqlQuerier) UpdateChatBuildAgentBinding(ctx context.Context, arg Update &i.GroupACL, &i.OwnerUsername, &i.OwnerName, + &i.ContextAggregateHash, + &i.ContextDirtySince, + &i.ContextDirtyResources, + &i.ContextError, ) return i, err } @@ -10939,7 +11159,7 @@ SET updated_at = NOW() WHERE id = $2::uuid -RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at +RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error ), chats_expanded AS ( SELECT @@ -10982,13 +11202,17 @@ chats_expanded AS ( COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl, COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl, owner.username AS owner_username, - owner.name AS owner_name + owner.name AS owner_name, + updated_chat.context_aggregate_hash, + updated_chat.context_dirty_since, + updated_chat.context_dirty_resources, + updated_chat.context_error FROM updated_chat LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id) JOIN visible_users owner ON owner.id = updated_chat.owner_id ) -SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name +SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error FROM chats_expanded ` @@ -11041,6 +11265,10 @@ func (q *sqlQuerier) UpdateChatByID(ctx context.Context, arg UpdateChatByIDParam &i.GroupACL, &i.OwnerUsername, &i.OwnerName, + &i.ContextAggregateHash, + &i.ContextDirtySince, + &i.ContextDirtyResources, + &i.ContextError, ) return i, err } @@ -11058,7 +11286,7 @@ WITH updated_chat AS ( pin_order = CASE WHEN $2::boolean THEN 0 ELSE pin_order END, updated_at = NOW() WHERE id = $7::uuid - RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at + RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error ), chats_expanded AS ( SELECT @@ -11101,12 +11329,16 @@ chats_expanded AS ( COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl, COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl, owner.username AS owner_username, - owner.name AS owner_name + owner.name AS owner_name, + updated_chat.context_aggregate_hash, + updated_chat.context_dirty_since, + updated_chat.context_dirty_resources, + updated_chat.context_error FROM updated_chat LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id) JOIN visible_users owner ON owner.id = updated_chat.owner_id ) -SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name +SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error FROM chats_expanded ` @@ -11176,6 +11408,10 @@ func (q *sqlQuerier) UpdateChatExecutionState(ctx context.Context, arg UpdateCha &i.GroupACL, &i.OwnerUsername, &i.OwnerName, + &i.ContextAggregateHash, + &i.ContextDirtySince, + &i.ContextDirtyResources, + &i.ContextError, ) return i, err } @@ -11234,7 +11470,7 @@ SET updated_at = NOW() WHERE id = $2::uuid -RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at +RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error ), chats_expanded AS ( SELECT @@ -11277,13 +11513,17 @@ chats_expanded AS ( COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl, COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl, owner.username AS owner_username, - owner.name AS owner_name + owner.name AS owner_name, + updated_chat.context_aggregate_hash, + updated_chat.context_dirty_since, + updated_chat.context_dirty_resources, + updated_chat.context_error FROM updated_chat LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id) JOIN visible_users owner ON owner.id = updated_chat.owner_id ) -SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name +SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error FROM chats_expanded ` @@ -11336,6 +11576,10 @@ func (q *sqlQuerier) UpdateChatLabelsByID(ctx context.Context, arg UpdateChatLab &i.GroupACL, &i.OwnerUsername, &i.OwnerName, + &i.ContextAggregateHash, + &i.ContextDirtySince, + &i.ContextDirtyResources, + &i.ContextError, ) return i, err } @@ -11346,7 +11590,7 @@ UPDATE chats SET last_injected_context = $1::jsonb WHERE id = $2::uuid -RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at +RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error ), chats_expanded AS ( SELECT @@ -11389,13 +11633,17 @@ chats_expanded AS ( COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl, COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl, owner.username AS owner_username, - owner.name AS owner_name + owner.name AS owner_name, + updated_chat.context_aggregate_hash, + updated_chat.context_dirty_since, + updated_chat.context_dirty_resources, + updated_chat.context_error FROM updated_chat LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id) JOIN visible_users owner ON owner.id = updated_chat.owner_id ) -SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name +SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error FROM chats_expanded ` @@ -11452,6 +11700,10 @@ func (q *sqlQuerier) UpdateChatLastInjectedContext(ctx context.Context, arg Upda &i.GroupACL, &i.OwnerUsername, &i.OwnerName, + &i.ContextAggregateHash, + &i.ContextDirtySince, + &i.ContextDirtyResources, + &i.ContextError, ) return i, err } @@ -11465,7 +11717,7 @@ SET last_model_config_id = $1::uuid WHERE id = $2::uuid -RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at +RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error ), chats_expanded AS ( SELECT @@ -11508,13 +11760,17 @@ chats_expanded AS ( COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl, COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl, owner.username AS owner_username, - owner.name AS owner_name + owner.name AS owner_name, + updated_chat.context_aggregate_hash, + updated_chat.context_dirty_since, + updated_chat.context_dirty_resources, + updated_chat.context_error FROM updated_chat LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id) JOIN visible_users owner ON owner.id = updated_chat.owner_id ) -SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name +SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error FROM chats_expanded ` @@ -11567,6 +11823,10 @@ func (q *sqlQuerier) UpdateChatLastModelConfigByID(ctx context.Context, arg Upda &i.GroupACL, &i.OwnerUsername, &i.OwnerName, + &i.ContextAggregateHash, + &i.ContextDirtySince, + &i.ContextDirtyResources, + &i.ContextError, ) return i, err } @@ -11630,7 +11890,7 @@ SET updated_at = NOW() WHERE id = $2::uuid -RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at +RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error ), chats_expanded AS ( SELECT @@ -11673,13 +11933,17 @@ chats_expanded AS ( COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl, COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl, owner.username AS owner_username, - owner.name AS owner_name + owner.name AS owner_name, + updated_chat.context_aggregate_hash, + updated_chat.context_dirty_since, + updated_chat.context_dirty_resources, + updated_chat.context_error FROM updated_chat LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id) JOIN visible_users owner ON owner.id = updated_chat.owner_id ) -SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name +SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error FROM chats_expanded ` @@ -11732,6 +11996,10 @@ func (q *sqlQuerier) UpdateChatMCPServerIDs(ctx context.Context, arg UpdateChatM &i.GroupACL, &i.OwnerUsername, &i.OwnerName, + &i.ContextAggregateHash, + &i.ContextDirtySince, + &i.ContextDirtyResources, + &i.ContextError, ) return i, err } @@ -11865,7 +12133,7 @@ SET plan_mode = $1::chat_plan_mode WHERE id = $2::uuid -RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at +RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error ), chats_expanded AS ( SELECT @@ -11908,13 +12176,17 @@ chats_expanded AS ( COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl, COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl, owner.username AS owner_username, - owner.name AS owner_name + owner.name AS owner_name, + updated_chat.context_aggregate_hash, + updated_chat.context_dirty_since, + updated_chat.context_dirty_resources, + updated_chat.context_error FROM updated_chat LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id) JOIN visible_users owner ON owner.id = updated_chat.owner_id ) -SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name +SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error FROM chats_expanded ` @@ -11967,6 +12239,10 @@ func (q *sqlQuerier) UpdateChatPlanModeByID(ctx context.Context, arg UpdateChatP &i.GroupACL, &i.OwnerUsername, &i.OwnerName, + &i.ContextAggregateHash, + &i.ContextDirtySince, + &i.ContextDirtyResources, + &i.ContextError, ) return i, err } @@ -11978,7 +12254,7 @@ WITH updated_chat AS ( retry_state = $1::jsonb, updated_at = NOW() WHERE id = $2::uuid - RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at + RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error ), chats_expanded AS ( SELECT @@ -12021,12 +12297,16 @@ chats_expanded AS ( COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl, COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl, owner.username AS owner_username, - owner.name AS owner_name + owner.name AS owner_name, + updated_chat.context_aggregate_hash, + updated_chat.context_dirty_since, + updated_chat.context_dirty_resources, + updated_chat.context_error FROM updated_chat LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id) JOIN visible_users owner ON owner.id = updated_chat.owner_id ) -SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name +SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error FROM chats_expanded ` @@ -12081,6 +12361,10 @@ func (q *sqlQuerier) UpdateChatRetryState(ctx context.Context, arg UpdateChatRet &i.GroupACL, &i.OwnerUsername, &i.OwnerName, + &i.ContextAggregateHash, + &i.ContextDirtySince, + &i.ContextDirtyResources, + &i.ContextError, ) return i, err } @@ -12098,7 +12382,7 @@ SET updated_at = NOW() WHERE id = $6::uuid -RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at +RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error ), chats_expanded AS ( SELECT @@ -12141,13 +12425,17 @@ chats_expanded AS ( COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl, COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl, owner.username AS owner_username, - owner.name AS owner_name + owner.name AS owner_name, + updated_chat.context_aggregate_hash, + updated_chat.context_dirty_since, + updated_chat.context_dirty_resources, + updated_chat.context_error FROM updated_chat LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id) JOIN visible_users owner ON owner.id = updated_chat.owner_id ) -SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name +SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error FROM chats_expanded ` @@ -12211,6 +12499,10 @@ func (q *sqlQuerier) UpdateChatStatus(ctx context.Context, arg UpdateChatStatusP &i.GroupACL, &i.OwnerUsername, &i.OwnerName, + &i.ContextAggregateHash, + &i.ContextDirtySince, + &i.ContextDirtyResources, + &i.ContextError, ) return i, err } @@ -12228,7 +12520,7 @@ SET updated_at = $6::timestamptz WHERE id = $7::uuid -RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at +RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error ), chats_expanded AS ( SELECT @@ -12271,13 +12563,17 @@ chats_expanded AS ( COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl, COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl, owner.username AS owner_username, - owner.name AS owner_name + owner.name AS owner_name, + updated_chat.context_aggregate_hash, + updated_chat.context_dirty_since, + updated_chat.context_dirty_resources, + updated_chat.context_error FROM updated_chat LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id) JOIN visible_users owner ON owner.id = updated_chat.owner_id ) -SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name +SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error FROM chats_expanded ` @@ -12343,6 +12639,10 @@ func (q *sqlQuerier) UpdateChatStatusPreserveUpdatedAt(ctx context.Context, arg &i.GroupACL, &i.OwnerUsername, &i.OwnerName, + &i.ContextAggregateHash, + &i.ContextDirtySince, + &i.ContextDirtyResources, + &i.ContextError, ) return i, err } @@ -12358,7 +12658,7 @@ SET title = $1::text WHERE id = $2::uuid -RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at +RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error ), chats_expanded AS ( SELECT @@ -12401,13 +12701,17 @@ chats_expanded AS ( COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl, COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl, owner.username AS owner_username, - owner.name AS owner_name + owner.name AS owner_name, + updated_chat.context_aggregate_hash, + updated_chat.context_dirty_since, + updated_chat.context_dirty_resources, + updated_chat.context_error FROM updated_chat LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id) JOIN visible_users owner ON owner.id = updated_chat.owner_id ) -SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name +SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error FROM chats_expanded ` @@ -12460,6 +12764,10 @@ func (q *sqlQuerier) UpdateChatTitleByID(ctx context.Context, arg UpdateChatTitl &i.GroupACL, &i.OwnerUsername, &i.OwnerName, + &i.ContextAggregateHash, + &i.ContextDirtySince, + &i.ContextDirtyResources, + &i.ContextError, ) return i, err } @@ -12472,7 +12780,7 @@ UPDATE chats SET agent_id = $3::uuid, updated_at = NOW() WHERE id = $4::uuid -RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at +RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error ), chats_expanded AS ( SELECT @@ -12515,13 +12823,17 @@ chats_expanded AS ( COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl, COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl, owner.username AS owner_username, - owner.name AS owner_name + owner.name AS owner_name, + updated_chat.context_aggregate_hash, + updated_chat.context_dirty_since, + updated_chat.context_dirty_resources, + updated_chat.context_error FROM updated_chat LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id) JOIN visible_users owner ON owner.id = updated_chat.owner_id ) -SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name +SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error FROM chats_expanded ` @@ -12581,6 +12893,10 @@ func (q *sqlQuerier) UpdateChatWorkspaceBinding(ctx context.Context, arg UpdateC &i.GroupACL, &i.OwnerUsername, &i.OwnerName, + &i.ContextAggregateHash, + &i.ContextDirtySince, + &i.ContextDirtyResources, + &i.ContextError, ) return i, err } diff --git a/coderd/database/queries/chats.sql b/coderd/database/queries/chats.sql index a395429f073ac..36234f3ea2d71 100644 --- a/coderd/database/queries/chats.sql +++ b/coderd/database/queries/chats.sql @@ -46,7 +46,11 @@ chats_expanded AS ( COALESCE(root.user_acl, updated_chats.user_acl) AS user_acl, COALESCE(root.group_acl, updated_chats.group_acl) AS group_acl, owner.username AS owner_username, - owner.name AS owner_name + owner.name AS owner_name, + updated_chats.context_aggregate_hash, + updated_chats.context_dirty_since, + updated_chats.context_dirty_resources, + updated_chats.context_error FROM updated_chats LEFT JOIN chats root ON root.id = COALESCE(updated_chats.root_chat_id, updated_chats.parent_chat_id) @@ -109,7 +113,11 @@ chats_expanded AS ( COALESCE(root.user_acl, updated_chats.user_acl) AS user_acl, COALESCE(root.group_acl, updated_chats.group_acl) AS group_acl, owner.username AS owner_username, - owner.name AS owner_name + owner.name AS owner_name, + updated_chats.context_aggregate_hash, + updated_chats.context_dirty_since, + updated_chats.context_dirty_resources, + updated_chats.context_error FROM updated_chats LEFT JOIN chats root ON root.id = COALESCE(updated_chats.root_chat_id, updated_chats.parent_chat_id) @@ -771,7 +779,11 @@ chats_expanded AS ( COALESCE(root.user_acl, inserted_chat.user_acl) AS user_acl, COALESCE(root.group_acl, inserted_chat.group_acl) AS group_acl, owner.username AS owner_username, - owner.name AS owner_name + owner.name AS owner_name, + inserted_chat.context_aggregate_hash, + inserted_chat.context_dirty_since, + inserted_chat.context_dirty_resources, + inserted_chat.context_error FROM inserted_chat LEFT JOIN chats root ON root.id = COALESCE(inserted_chat.root_chat_id, inserted_chat.parent_chat_id) @@ -916,7 +928,11 @@ chats_expanded AS ( COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl, COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl, owner.username AS owner_username, - owner.name AS owner_name + owner.name AS owner_name, + updated_chat.context_aggregate_hash, + updated_chat.context_dirty_since, + updated_chat.context_dirty_resources, + updated_chat.context_error FROM updated_chat LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id) @@ -979,7 +995,11 @@ chats_expanded AS ( COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl, COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl, owner.username AS owner_username, - owner.name AS owner_name + owner.name AS owner_name, + updated_chat.context_aggregate_hash, + updated_chat.context_dirty_since, + updated_chat.context_dirty_resources, + updated_chat.context_error FROM updated_chat LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id) @@ -1040,7 +1060,11 @@ chats_expanded AS ( COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl, COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl, owner.username AS owner_username, - owner.name AS owner_name + owner.name AS owner_name, + updated_chat.context_aggregate_hash, + updated_chat.context_dirty_since, + updated_chat.context_dirty_resources, + updated_chat.context_error FROM updated_chat LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id) @@ -1101,7 +1125,11 @@ chats_expanded AS ( COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl, COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl, owner.username AS owner_username, - owner.name AS owner_name + owner.name AS owner_name, + updated_chat.context_aggregate_hash, + updated_chat.context_dirty_since, + updated_chat.context_dirty_resources, + updated_chat.context_error FROM updated_chat LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id) @@ -1162,7 +1190,11 @@ chats_expanded AS ( COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl, COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl, owner.username AS owner_username, - owner.name AS owner_name + owner.name AS owner_name, + updated_chat.context_aggregate_hash, + updated_chat.context_dirty_since, + updated_chat.context_dirty_resources, + updated_chat.context_error FROM updated_chat LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id) @@ -1222,7 +1254,11 @@ chats_expanded AS ( COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl, COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl, owner.username AS owner_username, - owner.name AS owner_name + owner.name AS owner_name, + updated_chat.context_aggregate_hash, + updated_chat.context_dirty_since, + updated_chat.context_dirty_resources, + updated_chat.context_error FROM updated_chat LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id) @@ -1282,7 +1318,11 @@ chats_expanded AS ( COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl, COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl, owner.username AS owner_username, - owner.name AS owner_name + owner.name AS owner_name, + updated_chat.context_aggregate_hash, + updated_chat.context_dirty_since, + updated_chat.context_dirty_resources, + updated_chat.context_error FROM updated_chat LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id) @@ -1344,7 +1384,11 @@ chats_expanded AS ( COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl, COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl, owner.username AS owner_username, - owner.name AS owner_name + owner.name AS owner_name, + updated_chat.context_aggregate_hash, + updated_chat.context_dirty_since, + updated_chat.context_dirty_resources, + updated_chat.context_error FROM updated_chat LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id) @@ -1422,7 +1466,11 @@ chats_expanded AS ( COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl, COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl, owner.username AS owner_username, - owner.name AS owner_name + owner.name AS owner_name, + updated_chat.context_aggregate_hash, + updated_chat.context_dirty_since, + updated_chat.context_dirty_resources, + updated_chat.context_error FROM updated_chat LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id) @@ -1431,6 +1479,47 @@ chats_expanded AS ( SELECT * FROM chats_expanded; +-- name: SetChatContextSnapshot :exec +-- Pins a single chat to the supplied context snapshot hash and error +-- and clears any dirty marker. Used by chat-create hydration and the +-- refresh endpoint. Does not bump updated_at: context pinning is +-- background state and must not reorder chat lists. +UPDATE chats +SET + context_aggregate_hash = @aggregate_hash, + context_error = @context_error, + context_dirty_since = NULL +WHERE id = @id::uuid; + +-- name: HydrateAgentChatsContext :exec +-- Stamps the pinned hash and error on every not-yet-hydrated chat for +-- an agent (context_aggregate_hash IS NULL). Runs as a side effect of +-- an agent push so chats created before the agent was ready pick up the +-- snapshot without a dirty event. Does not bump updated_at. +UPDATE chats +SET + context_aggregate_hash = @aggregate_hash, + context_error = @context_error +WHERE agent_id = @agent_id::uuid + AND archived = false + AND context_aggregate_hash IS NULL; + +-- name: MarkChatsContextDirtyByAgent :many +-- Flips active, already-hydrated chats for an agent to dirty when the +-- agent's latest snapshot hash differs from the chat's pinned hash. The +-- pinned hash is intentionally left untouched; the refresh endpoint +-- re-pins it. Returns the chats that transitioned so the caller can +-- emit watch events after the transaction commits. +UPDATE chats +SET context_dirty_since = @dirty_since +WHERE agent_id = @agent_id::uuid + AND archived = false + AND status IN ('waiting', 'running', 'paused', 'pending', 'requires_action') + AND context_aggregate_hash IS NOT NULL + AND context_aggregate_hash IS DISTINCT FROM @aggregate_hash + AND context_dirty_since IS NULL +RETURNING id, owner_id; + -- name: LinkChatFiles :one -- LinkChatFiles inserts file associations into the chat_file_links -- join table with deduplication (ON CONFLICT DO NOTHING). The INSERT @@ -1539,7 +1628,11 @@ chats_expanded AS ( COALESCE(root.user_acl, acquired_chats.user_acl) AS user_acl, COALESCE(root.group_acl, acquired_chats.group_acl) AS group_acl, owner.username AS owner_username, - owner.name AS owner_name + owner.name AS owner_name, + acquired_chats.context_aggregate_hash, + acquired_chats.context_dirty_since, + acquired_chats.context_dirty_resources, + acquired_chats.context_error FROM acquired_chats LEFT JOIN chats root ON root.id = COALESCE(acquired_chats.root_chat_id, acquired_chats.parent_chat_id) @@ -1604,7 +1697,11 @@ chats_expanded AS ( COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl, COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl, owner.username AS owner_username, - owner.name AS owner_name + owner.name AS owner_name, + updated_chat.context_aggregate_hash, + updated_chat.context_dirty_since, + updated_chat.context_dirty_resources, + updated_chat.context_error FROM updated_chat LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id) @@ -1669,7 +1766,11 @@ chats_expanded AS ( COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl, COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl, owner.username AS owner_username, - owner.name AS owner_name + owner.name AS owner_name, + updated_chat.context_aggregate_hash, + updated_chat.context_dirty_since, + updated_chat.context_dirty_resources, + updated_chat.context_error FROM updated_chat LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id) @@ -1941,7 +2042,11 @@ chats_expanded AS ( COALESCE(root.user_acl, locked_chat.user_acl) AS user_acl, COALESCE(root.group_acl, locked_chat.group_acl) AS group_acl, owner.username AS owner_username, - owner.name AS owner_name + owner.name AS owner_name, + locked_chat.context_aggregate_hash, + locked_chat.context_dirty_since, + locked_chat.context_dirty_resources, + locked_chat.context_error FROM locked_chat LEFT JOIN chats root ON root.id = COALESCE(locked_chat.root_chat_id, locked_chat.parent_chat_id) @@ -1998,7 +2103,11 @@ chats_expanded AS ( COALESCE(root.user_acl, shared_chat.user_acl) AS user_acl, COALESCE(root.group_acl, shared_chat.group_acl) AS group_acl, owner.username AS owner_username, - owner.name AS owner_name + owner.name AS owner_name, + shared_chat.context_aggregate_hash, + shared_chat.context_dirty_since, + shared_chat.context_dirty_resources, + shared_chat.context_error FROM shared_chat LEFT JOIN chats root ON root.id = COALESCE(shared_chat.root_chat_id, shared_chat.parent_chat_id) @@ -2675,7 +2784,11 @@ chats_expanded AS ( COALESCE(root.user_acl, bumped_chat.user_acl) AS user_acl, COALESCE(root.group_acl, bumped_chat.group_acl) AS group_acl, owner.username AS owner_username, - owner.name AS owner_name + owner.name AS owner_name, + bumped_chat.context_aggregate_hash, + bumped_chat.context_dirty_since, + bumped_chat.context_dirty_resources, + bumped_chat.context_error FROM bumped_chat LEFT JOIN chats root ON root.id = COALESCE(bumped_chat.root_chat_id, bumped_chat.parent_chat_id) JOIN visible_users owner ON owner.id = bumped_chat.owner_id @@ -2743,7 +2856,11 @@ chats_expanded AS ( COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl, COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl, owner.username AS owner_username, - owner.name AS owner_name + owner.name AS owner_name, + updated_chat.context_aggregate_hash, + updated_chat.context_dirty_since, + updated_chat.context_dirty_resources, + updated_chat.context_error FROM updated_chat LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id) JOIN visible_users owner ON owner.id = updated_chat.owner_id @@ -2803,7 +2920,11 @@ chats_expanded AS ( COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl, COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl, owner.username AS owner_username, - owner.name AS owner_name + owner.name AS owner_name, + updated_chat.context_aggregate_hash, + updated_chat.context_dirty_since, + updated_chat.context_dirty_resources, + updated_chat.context_error FROM updated_chat LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id) JOIN visible_users owner ON owner.id = updated_chat.owner_id diff --git a/docs/admin/security/audit-logs.md b/docs/admin/security/audit-logs.md index c572565d34b11..be57d1c762487 100644 --- a/docs/admin/security/audit-logs.md +++ b/docs/admin/security/audit-logs.md @@ -13,41 +13,41 @@ We track the following resources: -| Resource | | | -|-----------------------------------------------------------------|----------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| AIGatewayKey
create, delete | |
FieldTracked
created_atfalse
hashed_secrettrue
idtrue
last_used_atfalse
nametrue
secret_prefixtrue
| -| AIProvider
create, write, delete | |
FieldTracked
base_urltrue
created_atfalse
deletedtrue
display_nametrue
enabledtrue
idtrue
nametrue
settingstrue
settings_key_idfalse
typetrue
updated_atfalse
| -| AIProviderKey
create, delete | |
FieldTracked
api_keytrue
api_key_key_idfalse
created_atfalse
idtrue
provider_idtrue
updated_atfalse
| -| APIKey
login, logout, register, create, write, delete | |
FieldTracked
allow_listfalse
created_attrue
expires_attrue
hashed_secretfalse
idfalse
ip_addressfalse
last_usedtrue
lifetime_secondsfalse
login_typefalse
scopesfalse
token_namefalse
updated_atfalse
user_idtrue
| -| AiSeatState
create | |
FieldTracked
first_used_attrue
last_event_descriptiontrue
last_event_typetrue
last_used_atfalse
updated_atfalse
user_idtrue
| -| AuditOAuthConvertState
| |
FieldTracked
created_attrue
expires_attrue
from_login_typetrue
to_login_typetrue
user_idtrue
| -| Group
create, write, delete | |
FieldTracked
avatar_urltrue
chat_spend_limit_microstrue
display_nametrue
idtrue
memberstrue
nametrue
organization_idfalse
quota_allowancetrue
sourcefalse
| -| AuditableGroupAiBudget
write, delete | |
FieldTracked
created_atfalse
group_idfalse
group_namefalse
spend_limittrue
spend_limit_microsfalse
updated_atfalse
| -| AuditableOrganizationMember
| |
FieldTracked
created_attrue
organization_idfalse
rolestrue
updated_attrue
user_idtrue
usernametrue
| -| AuditableUserAiBudgetOverride
write, delete | |
FieldTracked
created_atfalse
group_idtrue
group_nametrue
spend_limittrue
spend_limit_microsfalse
updated_atfalse
user_idfalse
usernamefalse
| -| Chat
create, write | |
FieldTracked
agent_idfalse
archivedtrue
build_idfalse
client_typefalse
created_atfalse
dynamic_toolsfalse
generation_attemptfalse
group_acltrue
heartbeat_atfalse
history_versionfalse
idtrue
labelstrue
last_errorfalse
last_injected_contextfalse
last_model_config_idfalse
last_read_message_idfalse
last_turn_summaryfalse
mcp_server_idstrue
modetrue
organization_idfalse
owner_idtrue
owner_namefalse
owner_usernamefalse
parent_chat_idfalse
pin_ordertrue
plan_modefalse
queue_versionfalse
requires_action_deadline_atfalse
retry_statefalse
retry_state_versionfalse
root_chat_idfalse
runner_idfalse
snapshot_versionfalse
started_atfalse
statusfalse
titletrue
updated_atfalse
user_acltrue
worker_idfalse
workspace_idtrue
| -| CustomRole
| |
FieldTracked
created_atfalse
display_nametrue
idfalse
is_systemfalse
member_permissionstrue
nametrue
org_permissionstrue
organization_idfalse
site_permissionstrue
updated_atfalse
user_permissionstrue
| -| GitSSHKey
create | |
FieldTracked
created_atfalse
private_keytrue
private_key_key_idfalse
public_keytrue
updated_atfalse
user_idtrue
| -| GroupSyncSettings
| |
FieldTracked
auto_create_missing_groupstrue
fieldtrue
legacy_group_name_mappingfalse
mappingtrue
regex_filtertrue
| -| HealthSettings
| |
FieldTracked
dismissed_healthcheckstrue
idfalse
| -| License
create, delete | |
FieldTracked
exptrue
idfalse
jwtfalse
uploaded_attrue
uuidtrue
| -| NotificationTemplate
| |
FieldTracked
actionstrue
body_templatetrue
enabled_by_defaulttrue
grouptrue
idfalse
kindtrue
methodtrue
nametrue
title_templatetrue
| -| NotificationsSettings
| |
FieldTracked
idfalse
notifier_pausedtrue
| -| OAuth2ProviderApp
| |
FieldTracked
callback_urltrue
client_id_issued_atfalse
client_secret_expires_attrue
client_typetrue
client_uritrue
contactstrue
created_atfalse
dynamically_registeredtrue
grant_typestrue
icontrue
idfalse
jwkstrue
jwks_uritrue
logo_uritrue
nametrue
policy_uritrue
redirect_uristrue
registration_access_tokentrue
registration_client_uritrue
response_typestrue
scopetrue
software_idtrue
software_versiontrue
token_endpoint_auth_methodtrue
tos_uritrue
updated_atfalse
| -| OAuth2ProviderAppSecret
| |
FieldTracked
app_idfalse
created_atfalse
display_secretfalse
hashed_secretfalse
idfalse
last_used_atfalse
secret_prefixfalse
| -| Organization
| |
FieldTracked
created_atfalse
default_org_member_rolestrue
deletedtrue
descriptiontrue
display_nametrue
icontrue
idfalse
is_defaulttrue
nametrue
shareable_workspace_ownerstrue
updated_attrue
| -| OrganizationSyncSettings
| |
FieldTracked
assign_defaulttrue
fieldtrue
mappingtrue
| -| PrebuildsSettings
| |
FieldTracked
idfalse
reconciliation_pausedtrue
| -| RoleSyncSettings
| |
FieldTracked
fieldtrue
mappingtrue
| -| TaskTable
| |
FieldTracked
created_atfalse
deleted_atfalse
display_nametrue
idtrue
nametrue
organization_idfalse
owner_idtrue
prompttrue
template_parameterstrue
template_version_idtrue
workspace_idtrue
| -| Template
write, delete | |
FieldTracked
active_version_idtrue
activity_bumptrue
allow_user_autostarttrue
allow_user_autostoptrue
allow_user_cancel_workspace_jobstrue
autostart_block_days_of_weektrue
autostop_requirement_days_of_weektrue
autostop_requirement_weekstrue
cors_behaviortrue
created_atfalse
created_bytrue
created_by_avatar_urlfalse
created_by_namefalse
created_by_usernamefalse
default_ttltrue
deletedfalse
deprecatedtrue
descriptiontrue
disable_module_cachetrue
display_nametrue
failure_ttltrue
group_acltrue
icontrue
idtrue
max_port_sharing_leveltrue
nametrue
organization_display_namefalse
organization_iconfalse
organization_idfalse
organization_namefalse
provisionertrue
require_active_versiontrue
time_til_dormanttrue
time_til_dormant_autodeletetrue
updated_atfalse
use_classic_parameter_flowtrue
user_acltrue
| -| TemplateVersion
create, write | |
FieldTracked
archivedtrue
created_atfalse
created_bytrue
created_by_avatar_urlfalse
created_by_namefalse
created_by_usernamefalse
external_auth_providersfalse
has_ai_taskfalse
has_external_agentfalse
idtrue
job_idfalse
messagefalse
nametrue
organization_idfalse
readmetrue
source_example_idfalse
template_idtrue
updated_atfalse
| -| User
create, write, delete | |
FieldTracked
avatar_urlfalse
chat_spend_limit_microstrue
created_atfalse
deletedtrue
emailtrue
github_com_user_idfalse
hashed_one_time_passcodefalse
hashed_passwordtrue
idtrue
is_service_accounttrue
is_systemtrue
last_seen_atfalse
login_typetrue
nametrue
one_time_passcode_expires_attrue
quiet_hours_scheduletrue
rbac_rolestrue
statustrue
updated_atfalse
usernametrue
| -| UserSecret
create, write, delete | |
FieldTracked
created_atfalse
descriptiontrue
env_nametrue
file_pathtrue
idtrue
nametrue
updated_atfalse
user_idtrue
valuetrue
value_key_idfalse
| -| UserSkill
create, write, delete | |
FieldTracked
contenttrue
created_atfalse
descriptiontrue
idtrue
nametrue
updated_atfalse
user_idtrue
| -| WorkspaceBuild
start, stop | |
FieldTracked
build_numberfalse
created_atfalse
daily_costfalse
deadlinefalse
has_ai_taskfalse
has_external_agentfalse
idfalse
initiator_by_avatar_urlfalse
initiator_by_namefalse
initiator_by_usernamefalse
initiator_idfalse
job_idfalse
max_deadlinefalse
reasonfalse
template_version_idtrue
template_version_preset_idfalse
transitionfalse
updated_atfalse
workspace_idfalse
| -| WorkspaceProxy
| |
FieldTracked
created_attrue
deletedfalse
derp_enabledtrue
derp_onlytrue
display_nametrue
icontrue
idtrue
nametrue
region_idtrue
token_hashed_secrettrue
updated_atfalse
urltrue
versiontrue
wildcard_hostnametrue
| -| WorkspaceTable
| |
FieldTracked
automatic_updatestrue
autostart_scheduletrue
created_atfalse
deletedfalse
deleting_attrue
dormant_attrue
favoritetrue
group_acltrue
idtrue
last_used_atfalse
nametrue
next_start_attrue
organization_idfalse
owner_idtrue
template_idtrue
ttltrue
updated_atfalse
user_acltrue
| +| Resource | | | +|-----------------------------------------------------------------|----------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| AIGatewayKey
create, delete | |
FieldTracked
created_atfalse
hashed_secrettrue
idtrue
last_used_atfalse
nametrue
secret_prefixtrue
| +| AIProvider
create, write, delete | |
FieldTracked
base_urltrue
created_atfalse
deletedtrue
display_nametrue
enabledtrue
idtrue
nametrue
settingstrue
settings_key_idfalse
typetrue
updated_atfalse
| +| AIProviderKey
create, delete | |
FieldTracked
api_keytrue
api_key_key_idfalse
created_atfalse
idtrue
provider_idtrue
updated_atfalse
| +| APIKey
login, logout, register, create, write, delete | |
FieldTracked
allow_listfalse
created_attrue
expires_attrue
hashed_secretfalse
idfalse
ip_addressfalse
last_usedtrue
lifetime_secondsfalse
login_typefalse
scopesfalse
token_namefalse
updated_atfalse
user_idtrue
| +| AiSeatState
create | |
FieldTracked
first_used_attrue
last_event_descriptiontrue
last_event_typetrue
last_used_atfalse
updated_atfalse
user_idtrue
| +| AuditOAuthConvertState
| |
FieldTracked
created_attrue
expires_attrue
from_login_typetrue
to_login_typetrue
user_idtrue
| +| Group
create, write, delete | |
FieldTracked
avatar_urltrue
chat_spend_limit_microstrue
display_nametrue
idtrue
memberstrue
nametrue
organization_idfalse
quota_allowancetrue
sourcefalse
| +| AuditableGroupAiBudget
write, delete | |
FieldTracked
created_atfalse
group_idfalse
group_namefalse
spend_limittrue
spend_limit_microsfalse
updated_atfalse
| +| AuditableOrganizationMember
| |
FieldTracked
created_attrue
organization_idfalse
rolestrue
updated_attrue
user_idtrue
usernametrue
| +| AuditableUserAiBudgetOverride
write, delete | |
FieldTracked
created_atfalse
group_idtrue
group_nametrue
spend_limittrue
spend_limit_microsfalse
updated_atfalse
user_idfalse
usernamefalse
| +| Chat
create, write | |
FieldTracked
agent_idfalse
archivedtrue
build_idfalse
client_typefalse
context_aggregate_hashfalse
context_dirty_resourcesfalse
context_dirty_sincefalse
context_errorfalse
created_atfalse
dynamic_toolsfalse
generation_attemptfalse
group_acltrue
heartbeat_atfalse
history_versionfalse
idtrue
labelstrue
last_errorfalse
last_injected_contextfalse
last_model_config_idfalse
last_read_message_idfalse
last_turn_summaryfalse
mcp_server_idstrue
modetrue
organization_idfalse
owner_idtrue
owner_namefalse
owner_usernamefalse
parent_chat_idfalse
pin_ordertrue
plan_modefalse
queue_versionfalse
requires_action_deadline_atfalse
retry_statefalse
retry_state_versionfalse
root_chat_idfalse
runner_idfalse
snapshot_versionfalse
started_atfalse
statusfalse
titletrue
updated_atfalse
user_acltrue
worker_idfalse
workspace_idtrue
| +| CustomRole
| |
FieldTracked
created_atfalse
display_nametrue
idfalse
is_systemfalse
member_permissionstrue
nametrue
org_permissionstrue
organization_idfalse
site_permissionstrue
updated_atfalse
user_permissionstrue
| +| GitSSHKey
create | |
FieldTracked
created_atfalse
private_keytrue
private_key_key_idfalse
public_keytrue
updated_atfalse
user_idtrue
| +| GroupSyncSettings
| |
FieldTracked
auto_create_missing_groupstrue
fieldtrue
legacy_group_name_mappingfalse
mappingtrue
regex_filtertrue
| +| HealthSettings
| |
FieldTracked
dismissed_healthcheckstrue
idfalse
| +| License
create, delete | |
FieldTracked
exptrue
idfalse
jwtfalse
uploaded_attrue
uuidtrue
| +| NotificationTemplate
| |
FieldTracked
actionstrue
body_templatetrue
enabled_by_defaulttrue
grouptrue
idfalse
kindtrue
methodtrue
nametrue
title_templatetrue
| +| NotificationsSettings
| |
FieldTracked
idfalse
notifier_pausedtrue
| +| OAuth2ProviderApp
| |
FieldTracked
callback_urltrue
client_id_issued_atfalse
client_secret_expires_attrue
client_typetrue
client_uritrue
contactstrue
created_atfalse
dynamically_registeredtrue
grant_typestrue
icontrue
idfalse
jwkstrue
jwks_uritrue
logo_uritrue
nametrue
policy_uritrue
redirect_uristrue
registration_access_tokentrue
registration_client_uritrue
response_typestrue
scopetrue
software_idtrue
software_versiontrue
token_endpoint_auth_methodtrue
tos_uritrue
updated_atfalse
| +| OAuth2ProviderAppSecret
| |
FieldTracked
app_idfalse
created_atfalse
display_secretfalse
hashed_secretfalse
idfalse
last_used_atfalse
secret_prefixfalse
| +| Organization
| |
FieldTracked
created_atfalse
default_org_member_rolestrue
deletedtrue
descriptiontrue
display_nametrue
icontrue
idfalse
is_defaulttrue
nametrue
shareable_workspace_ownerstrue
updated_attrue
| +| OrganizationSyncSettings
| |
FieldTracked
assign_defaulttrue
fieldtrue
mappingtrue
| +| PrebuildsSettings
| |
FieldTracked
idfalse
reconciliation_pausedtrue
| +| RoleSyncSettings
| |
FieldTracked
fieldtrue
mappingtrue
| +| TaskTable
| |
FieldTracked
created_atfalse
deleted_atfalse
display_nametrue
idtrue
nametrue
organization_idfalse
owner_idtrue
prompttrue
template_parameterstrue
template_version_idtrue
workspace_idtrue
| +| Template
write, delete | |
FieldTracked
active_version_idtrue
activity_bumptrue
allow_user_autostarttrue
allow_user_autostoptrue
allow_user_cancel_workspace_jobstrue
autostart_block_days_of_weektrue
autostop_requirement_days_of_weektrue
autostop_requirement_weekstrue
cors_behaviortrue
created_atfalse
created_bytrue
created_by_avatar_urlfalse
created_by_namefalse
created_by_usernamefalse
default_ttltrue
deletedfalse
deprecatedtrue
descriptiontrue
disable_module_cachetrue
display_nametrue
failure_ttltrue
group_acltrue
icontrue
idtrue
max_port_sharing_leveltrue
nametrue
organization_display_namefalse
organization_iconfalse
organization_idfalse
organization_namefalse
provisionertrue
require_active_versiontrue
time_til_dormanttrue
time_til_dormant_autodeletetrue
updated_atfalse
use_classic_parameter_flowtrue
user_acltrue
| +| TemplateVersion
create, write | |
FieldTracked
archivedtrue
created_atfalse
created_bytrue
created_by_avatar_urlfalse
created_by_namefalse
created_by_usernamefalse
external_auth_providersfalse
has_ai_taskfalse
has_external_agentfalse
idtrue
job_idfalse
messagefalse
nametrue
organization_idfalse
readmetrue
source_example_idfalse
template_idtrue
updated_atfalse
| +| User
create, write, delete | |
FieldTracked
avatar_urlfalse
chat_spend_limit_microstrue
created_atfalse
deletedtrue
emailtrue
github_com_user_idfalse
hashed_one_time_passcodefalse
hashed_passwordtrue
idtrue
is_service_accounttrue
is_systemtrue
last_seen_atfalse
login_typetrue
nametrue
one_time_passcode_expires_attrue
quiet_hours_scheduletrue
rbac_rolestrue
statustrue
updated_atfalse
usernametrue
| +| UserSecret
create, write, delete | |
FieldTracked
created_atfalse
descriptiontrue
env_nametrue
file_pathtrue
idtrue
nametrue
updated_atfalse
user_idtrue
valuetrue
value_key_idfalse
| +| UserSkill
create, write, delete | |
FieldTracked
contenttrue
created_atfalse
descriptiontrue
idtrue
nametrue
updated_atfalse
user_idtrue
| +| WorkspaceBuild
start, stop | |
FieldTracked
build_numberfalse
created_atfalse
daily_costfalse
deadlinefalse
has_ai_taskfalse
has_external_agentfalse
idfalse
initiator_by_avatar_urlfalse
initiator_by_namefalse
initiator_by_usernamefalse
initiator_idfalse
job_idfalse
max_deadlinefalse
reasonfalse
template_version_idtrue
template_version_preset_idfalse
transitionfalse
updated_atfalse
workspace_idfalse
| +| WorkspaceProxy
| |
FieldTracked
created_attrue
deletedfalse
derp_enabledtrue
derp_onlytrue
display_nametrue
icontrue
idtrue
nametrue
region_idtrue
token_hashed_secrettrue
updated_atfalse
urltrue
versiontrue
wildcard_hostnametrue
| +| WorkspaceTable
| |
FieldTracked
automatic_updatestrue
autostart_scheduletrue
created_atfalse
deletedfalse
deleting_attrue
dormant_attrue
favoritetrue
group_acltrue
idtrue
last_used_atfalse
nametrue
next_start_attrue
organization_idfalse
owner_idtrue
template_idtrue
ttltrue
updated_atfalse
user_acltrue
| diff --git a/enterprise/audit/table.go b/enterprise/audit/table.go index bcbc2b469b6d5..23a9c4f44b2eb 100644 --- a/enterprise/audit/table.go +++ b/enterprise/audit/table.go @@ -465,6 +465,10 @@ var auditableResourcesTypes = map[any]map[string]Action{ "pin_order": ActionTrack, "last_read_message_id": ActionIgnore, // User-scoped read cursor. "last_injected_context": ActionIgnore, // Internal lifecycle. + "context_aggregate_hash": ActionIgnore, // Agent-pushed context snapshot state. + "context_dirty_since": ActionIgnore, // Agent-pushed context snapshot state. + "context_dirty_resources": ActionIgnore, // Agent-pushed context snapshot state. + "context_error": ActionIgnore, // Agent-pushed context snapshot state. "dynamic_tools": ActionIgnore, // Internal lifecycle. "plan_mode": ActionIgnore, // Can flip back and forth during a session. "client_type": ActionIgnore, // Set at creation.