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.