mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: auto-archive inactive chats with audit trail (#24642)
Adds a background job in `dbpurge` that periodically archives chats inactive beyond a configurable threshold. Each archived root chat gets a background audit entry tagged `chat_auto_archive`. Disabled by default. * New `AutoArchiveInactiveChats` SQL query with LATERAL last-activity subquery and partial index on archive candidates * `site_configs`-backed `auto_archive_days` setting with admin-only PUT, any-authenticated-user GET * Cascade archive via `root_chat_id`; pinned chats and active threads exempt * Root-only audit dispatch on detached context, matching manual archive (`patchChat`) behavior * 11 subtests covering disabled no-op, boundary, deleted messages, child activity, pinned exemption, multi-owner, idempotency, and batch pagination PR #24643 adds per-owner digest notifications. PR #24704 adds the requisite UI controls. > 🤖
This commit is contained in:
@@ -1427,3 +1427,55 @@ UPDATE chat_messages SET deleted = true
|
||||
WHERE chat_id = @chat_id::uuid
|
||||
AND deleted = false
|
||||
AND content::jsonb @> '[{"type": "context-file"}]';
|
||||
|
||||
-- name: AutoArchiveInactiveChats :many
|
||||
-- Archives inactive root chats (pinned and already-archived chats skipped),
|
||||
-- cascading to children via root_chat_id. Limits apply to roots, not total
|
||||
-- rows. Used by dbpurge.
|
||||
WITH to_archive AS (
|
||||
SELECT
|
||||
c.id,
|
||||
-- Activity = MAX(cm.created_at) across the family, or c.created_at
|
||||
-- when the family has no non-deleted messages.
|
||||
COALESCE(activity.last_activity_at, c.created_at) AS last_activity_at
|
||||
FROM chats c
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT MAX(cm.created_at) AS last_activity_at
|
||||
FROM chat_messages cm
|
||||
JOIN chats fc ON fc.id = cm.chat_id
|
||||
WHERE (fc.id = c.id OR fc.root_chat_id = c.id)
|
||||
AND cm.deleted = false
|
||||
) activity ON TRUE
|
||||
WHERE c.archived = false
|
||||
AND c.pin_order = 0
|
||||
AND c.parent_chat_id IS NULL -- roots only
|
||||
AND c.created_at < @archive_cutoff::timestamptz
|
||||
-- New active statuses must be added here to prevent archiving.
|
||||
AND c.status NOT IN ('running', 'pending', 'paused', 'requires_action')
|
||||
AND COALESCE(activity.last_activity_at, c.created_at) < @archive_cutoff::timestamptz
|
||||
-- Sorting by created_at lets Postgres drive the scan from the
|
||||
-- partial index instead of evaluating every LATERAL subquery
|
||||
-- before sorting. All candidates are past the cutoff, so the
|
||||
-- archive order is immaterial once the backlog drains.
|
||||
ORDER BY c.created_at ASC
|
||||
LIMIT @limit_count
|
||||
),
|
||||
archived AS (
|
||||
UPDATE chats c
|
||||
SET archived = true, pin_order = 0, updated_at = NOW()
|
||||
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.*
|
||||
)
|
||||
SELECT
|
||||
a.*,
|
||||
-- Children inherit their root's activity so last_activity_at is never null.
|
||||
COALESCE(
|
||||
t.last_activity_at,
|
||||
(SELECT tr.last_activity_at FROM to_archive tr WHERE tr.id = a.root_chat_id),
|
||||
a.created_at
|
||||
)::timestamptz AS last_activity_at
|
||||
FROM archived a
|
||||
LEFT JOIN to_archive t ON t.id = a.id
|
||||
ORDER BY (a.root_chat_id IS NULL) DESC, a.owner_id ASC, a.created_at ASC, a.id ASC;
|
||||
|
||||
Reference in New Issue
Block a user