mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add chat and chat_files cleanup to dbpurge (#23833)
Fixes https://github.com/coder/coder/issues/23910 Adds periodic cleanup of chats and chat files to the dbpurge background goroutine, with a configurable retention period exposed in the Agent settings UI. > 🤖 Written by a Coder Agent. Reviewed by a human.
This commit is contained in:
@@ -3279,6 +3279,20 @@ class ExperimentalApiMethods {
|
||||
await this.axios.put("/api/experimental/chats/config/workspace-ttl", req);
|
||||
};
|
||||
|
||||
getChatRetentionDays =
|
||||
async (): Promise<TypesGen.ChatRetentionDaysResponse> => {
|
||||
const response = await this.axios.get<TypesGen.ChatRetentionDaysResponse>(
|
||||
"/api/experimental/chats/config/retention-days",
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
updateChatRetentionDays = async (
|
||||
req: TypesGen.UpdateChatRetentionDaysRequest,
|
||||
): Promise<void> => {
|
||||
await this.axios.put("/api/experimental/chats/config/retention-days", req);
|
||||
};
|
||||
|
||||
updateChatTemplateAllowlist = async (
|
||||
req: TypesGen.ChatTemplateAllowlist,
|
||||
): Promise<void> => {
|
||||
|
||||
@@ -770,6 +770,22 @@ export const updateChatWorkspaceTTL = (queryClient: QueryClient) => ({
|
||||
},
|
||||
});
|
||||
|
||||
const chatRetentionDaysKey = ["chat-retention-days"] as const;
|
||||
|
||||
export const chatRetentionDays = () => ({
|
||||
queryKey: chatRetentionDaysKey,
|
||||
queryFn: () => API.experimental.getChatRetentionDays(),
|
||||
});
|
||||
|
||||
export const updateChatRetentionDays = (queryClient: QueryClient) => ({
|
||||
mutationFn: API.experimental.updateChatRetentionDays,
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: chatRetentionDaysKey,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const chatTemplateAllowlistKey = ["chat-template-allowlist"] as const;
|
||||
|
||||
export const chatTemplateAllowlist = () => ({
|
||||
|
||||
Generated
+17
@@ -1903,6 +1903,14 @@ export interface ChatReasoningPart {
|
||||
readonly text: string;
|
||||
}
|
||||
|
||||
// From codersdk/chats.go
|
||||
/**
|
||||
* ChatRetentionDaysResponse contains the current chat retention setting.
|
||||
*/
|
||||
export interface ChatRetentionDaysResponse {
|
||||
readonly retention_days: number;
|
||||
}
|
||||
|
||||
// From codersdk/chats.go
|
||||
export interface ChatSkillPart {
|
||||
readonly type: "skill";
|
||||
@@ -7261,6 +7269,15 @@ export interface UpdateChatRequest {
|
||||
readonly labels?: Record<string, string>;
|
||||
}
|
||||
|
||||
// From codersdk/chats.go
|
||||
/**
|
||||
* UpdateChatRetentionDaysRequest is a request to update the chat
|
||||
* retention period.
|
||||
*/
|
||||
export interface UpdateChatRetentionDaysRequest {
|
||||
readonly retention_days: number;
|
||||
}
|
||||
|
||||
// From codersdk/chats.go
|
||||
/**
|
||||
* UpdateChatSystemPromptRequest is the request body for updating the chat
|
||||
|
||||
@@ -3,11 +3,13 @@ import { useMutation, useQuery, useQueryClient } from "react-query";
|
||||
import {
|
||||
chatDesktopEnabled,
|
||||
chatModelConfigs,
|
||||
chatRetentionDays,
|
||||
chatSystemPrompt,
|
||||
chatUserCustomPrompt,
|
||||
chatWorkspaceTTL,
|
||||
deleteUserCompactionThreshold,
|
||||
updateChatDesktopEnabled,
|
||||
updateChatRetentionDays,
|
||||
updateChatSystemPrompt,
|
||||
updateChatWorkspaceTTL,
|
||||
updateUserChatCustomPrompt,
|
||||
@@ -44,6 +46,11 @@ const AgentSettingsBehaviorPage: FC = () => {
|
||||
updateChatWorkspaceTTL(queryClient),
|
||||
);
|
||||
|
||||
const retentionDaysQuery = useQuery(chatRetentionDays());
|
||||
const saveRetentionDaysMutation = useMutation(
|
||||
updateChatRetentionDays(queryClient),
|
||||
);
|
||||
|
||||
const modelConfigsQuery = useQuery(chatModelConfigs());
|
||||
|
||||
const thresholdsQuery = useQuery(userCompactionThresholds());
|
||||
@@ -95,6 +102,12 @@ const AgentSettingsBehaviorPage: FC = () => {
|
||||
onSaveWorkspaceTTL={saveWorkspaceTTLMutation.mutate}
|
||||
isSavingWorkspaceTTL={saveWorkspaceTTLMutation.isPending}
|
||||
isSaveWorkspaceTTLError={saveWorkspaceTTLMutation.isError}
|
||||
retentionDaysData={retentionDaysQuery.data}
|
||||
isRetentionDaysLoading={retentionDaysQuery.isLoading}
|
||||
isRetentionDaysLoadError={retentionDaysQuery.isError}
|
||||
onSaveRetentionDays={saveRetentionDaysMutation.mutate}
|
||||
isSavingRetentionDays={saveRetentionDaysMutation.isPending}
|
||||
isSaveRetentionDaysError={saveRetentionDaysMutation.isError}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -39,6 +39,9 @@ interface AgentSettingsBehaviorPageViewProps {
|
||||
workspaceTTLData: TypesGen.ChatWorkspaceTTLResponse | undefined;
|
||||
isWorkspaceTTLLoading: boolean;
|
||||
isWorkspaceTTLLoadError: boolean;
|
||||
retentionDaysData: TypesGen.ChatRetentionDaysResponse | undefined;
|
||||
isRetentionDaysLoading: boolean;
|
||||
isRetentionDaysLoadError: boolean;
|
||||
modelConfigsData: TypesGen.ChatModelConfig[] | undefined;
|
||||
modelConfigsError: unknown;
|
||||
isLoadingModelConfigs: boolean;
|
||||
@@ -81,6 +84,13 @@ interface AgentSettingsBehaviorPageViewProps {
|
||||
) => void;
|
||||
isSavingWorkspaceTTL: boolean;
|
||||
isSaveWorkspaceTTLError: boolean;
|
||||
|
||||
onSaveRetentionDays: (
|
||||
req: TypesGen.UpdateChatRetentionDaysRequest,
|
||||
options?: MutationCallbacks,
|
||||
) => void;
|
||||
isSavingRetentionDays: boolean;
|
||||
isSaveRetentionDaysError: boolean;
|
||||
}
|
||||
|
||||
export const AgentSettingsBehaviorPageView: FC<
|
||||
@@ -93,6 +103,9 @@ export const AgentSettingsBehaviorPageView: FC<
|
||||
workspaceTTLData,
|
||||
isWorkspaceTTLLoading,
|
||||
isWorkspaceTTLLoadError,
|
||||
retentionDaysData,
|
||||
isRetentionDaysLoading,
|
||||
isRetentionDaysLoadError,
|
||||
modelConfigsData,
|
||||
modelConfigsError,
|
||||
isLoadingModelConfigs,
|
||||
@@ -113,6 +126,9 @@ export const AgentSettingsBehaviorPageView: FC<
|
||||
onSaveWorkspaceTTL,
|
||||
isSavingWorkspaceTTL,
|
||||
isSaveWorkspaceTTLError,
|
||||
onSaveRetentionDays,
|
||||
isSavingRetentionDays,
|
||||
isSaveRetentionDaysError,
|
||||
}) => {
|
||||
// ── Local form state ──
|
||||
const [localEdit, setLocalEdit] = useState<string | null>(null);
|
||||
@@ -124,6 +140,12 @@ export const AgentSettingsBehaviorPageView: FC<
|
||||
const [localUserEdit, setLocalUserEdit] = useState<string | null>(null);
|
||||
const [localTTLMs, setLocalTTLMs] = useState<number | null>(null);
|
||||
const [autostopToggled, setAutostopToggled] = useState<boolean | null>(null);
|
||||
const [localRetentionDays, setLocalRetentionDays] = useState<number | null>(
|
||||
null,
|
||||
);
|
||||
const [retentionToggled, setRetentionToggled] = useState<boolean | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
// Overflow states are pure UI — managed locally in the view.
|
||||
const [isUserPromptOverflowing, setIsUserPromptOverflowing] = useState(false);
|
||||
@@ -171,6 +193,18 @@ export const AgentSettingsBehaviorPageView: FC<
|
||||
const isTTLOverMax = ttlMs > maxTTLMs;
|
||||
const isTTLZero = isAutostopEnabled && ttlMs === 0;
|
||||
|
||||
// ── Retention days derived state ──
|
||||
const serverRetentionDays = retentionDaysData?.retention_days ?? 30;
|
||||
const retentionDays = localRetentionDays ?? serverRetentionDays;
|
||||
const isRetentionEnabled = retentionToggled ?? serverRetentionDays > 0;
|
||||
const isRetentionDaysDirty =
|
||||
localRetentionDays !== null && localRetentionDays !== serverRetentionDays;
|
||||
const isRetentionDaysNegative = isRetentionEnabled && retentionDays < 0;
|
||||
// Keep in sync with retentionDaysMaximum in coderd/exp_chats.go.
|
||||
const retentionDaysMaximum = 3650;
|
||||
const isRetentionDaysOverMax = retentionDays > retentionDaysMaximum;
|
||||
const isRetentionDaysZero = isRetentionEnabled && retentionDays === 0;
|
||||
|
||||
// ── Event handlers ──
|
||||
const handleSaveSystemPrompt = (event: FormEvent) => {
|
||||
event.preventDefault();
|
||||
@@ -245,6 +279,45 @@ export const AgentSettingsBehaviorPageView: FC<
|
||||
}
|
||||
};
|
||||
|
||||
const resetRetentionState = () => {
|
||||
setLocalRetentionDays(null);
|
||||
setRetentionToggled(null);
|
||||
};
|
||||
|
||||
const handleToggleRetention = (checked: boolean) => {
|
||||
if (checked) {
|
||||
setRetentionToggled(true);
|
||||
setLocalRetentionDays(serverRetentionDays > 0 ? serverRetentionDays : 30);
|
||||
onSaveRetentionDays(
|
||||
{ retention_days: serverRetentionDays > 0 ? serverRetentionDays : 30 },
|
||||
{ onSuccess: resetRetentionState, onError: resetRetentionState },
|
||||
);
|
||||
} else {
|
||||
setRetentionToggled(false);
|
||||
setLocalRetentionDays(0);
|
||||
onSaveRetentionDays(
|
||||
{ retention_days: 0 },
|
||||
{ onSuccess: resetRetentionState, onError: resetRetentionState },
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRetentionDaysChange = (value: number) => {
|
||||
setLocalRetentionDays(value);
|
||||
if (retentionToggled === null) {
|
||||
setRetentionToggled(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveRetentionDays = (event: FormEvent) => {
|
||||
event.preventDefault();
|
||||
if (!isRetentionDaysDirty || isSavingRetentionDays) return;
|
||||
onSaveRetentionDays(
|
||||
{ retention_days: localRetentionDays ?? 30 },
|
||||
{ onSuccess: resetRetentionState },
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<SectionHeader
|
||||
@@ -520,6 +593,89 @@ export const AgentSettingsBehaviorPageView: FC<
|
||||
</p>
|
||||
)}
|
||||
</form>
|
||||
<hr className="my-5 border-0 border-t border-solid border-border" />
|
||||
<form
|
||||
className="space-y-2"
|
||||
onSubmit={(event) => void handleSaveRetentionDays(event)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="m-0 text-[13px] font-semibold text-content-primary">
|
||||
Conversation Retention Period
|
||||
</h3>
|
||||
<AdminBadge />
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<p className="!mt-0.5 m-0 flex-1 text-xs text-content-secondary">
|
||||
Archived conversations and orphaned files older than this are
|
||||
automatically deleted.
|
||||
</p>
|
||||
<Switch
|
||||
checked={isRetentionEnabled}
|
||||
onCheckedChange={handleToggleRetention}
|
||||
aria-label="Enable conversation retention"
|
||||
disabled={isSavingRetentionDays || isRetentionDaysLoading}
|
||||
/>
|
||||
</div>
|
||||
{isRetentionEnabled && (
|
||||
<>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
max={3650}
|
||||
step={1}
|
||||
aria-label="Conversation retention period in days"
|
||||
value={retentionDays}
|
||||
onChange={(event) =>
|
||||
handleRetentionDaysChange(
|
||||
Number.parseInt(event.target.value, 10) || 0,
|
||||
)
|
||||
}
|
||||
disabled={isSavingRetentionDays || isRetentionDaysLoading}
|
||||
className="w-full rounded-lg border border-border bg-surface-primary px-4 py-2 text-[13px] text-content-primary placeholder:text-content-secondary focus:outline-none focus:ring-2 focus:ring-content-link/30"
|
||||
/>
|
||||
{isRetentionDaysZero && (
|
||||
<p className="m-0 text-xs text-content-destructive">
|
||||
Retention period must be at least 1 day.
|
||||
</p>
|
||||
)}
|
||||
{isRetentionDaysNegative && (
|
||||
<p className="m-0 text-xs text-content-destructive">
|
||||
Retention days cannot be negative.
|
||||
</p>
|
||||
)}
|
||||
{isRetentionDaysOverMax && (
|
||||
<p className="m-0 text-xs text-content-destructive">
|
||||
Must not exceed {retentionDaysMaximum} days (~10 years).
|
||||
</p>
|
||||
)}
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
size="sm"
|
||||
type="submit"
|
||||
disabled={
|
||||
isSavingRetentionDays ||
|
||||
!isRetentionDaysDirty ||
|
||||
isRetentionDaysNegative ||
|
||||
isRetentionDaysOverMax ||
|
||||
isRetentionDaysZero
|
||||
}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{isSaveRetentionDaysError && (
|
||||
<p className="m-0 text-xs text-content-destructive">
|
||||
Failed to save retention setting.
|
||||
</p>
|
||||
)}
|
||||
{isRetentionDaysLoadError && (
|
||||
<p className="m-0 text-xs text-content-destructive">
|
||||
Failed to load retention setting.
|
||||
</p>
|
||||
)}
|
||||
</form>
|
||||
</>
|
||||
)}
|
||||
<hr className="my-5 border-0 border-t border-solid border-border" />
|
||||
|
||||
@@ -182,6 +182,12 @@ const BehaviorRouteElement = () => {
|
||||
onSaveWorkspaceTTL={fn()}
|
||||
isSavingWorkspaceTTL={false}
|
||||
isSaveWorkspaceTTLError={false}
|
||||
retentionDaysData={{ retention_days: 30 }}
|
||||
isRetentionDaysLoading={false}
|
||||
isRetentionDaysLoadError={false}
|
||||
onSaveRetentionDays={fn()}
|
||||
isSavingRetentionDays={false}
|
||||
isSaveRetentionDaysError={false}
|
||||
onSaveThreshold={fn(async () => undefined)}
|
||||
onResetThreshold={fn(async () => undefined)}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user