Skip to content

feat: auto-archive inactive chats with audit trail#24642

Merged
johnstcn merged 7 commits into
mainfrom
cj/chat-archive-core
Apr 24, 2026
Merged

feat: auto-archive inactive chats with audit trail#24642
johnstcn merged 7 commits into
mainfrom
cj/chat-archive-core

Conversation

@johnstcn

@johnstcn johnstcn commented Apr 22, 2026

Copy link
Copy Markdown
Member

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

Stacked PR #24643 adds per-owner digest notifications.
Stacked PR #24704 adds the requisite UI controls.

🤖

@github-actions

Copy link
Copy Markdown

Docs preview

📖 View docs preview

@johnstcn johnstcn changed the title feat(coderd): auto-archive inactive chats with audit trail feat: auto-archive inactive chats with audit trail Apr 22, 2026
@johnstcn johnstcn force-pushed the cj/chat-archive-core branch from d8d9e0e to 4583b50 Compare April 22, 2026 20:27

@johnstcn johnstcn left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖

Clean implementation. The batch-paginated archive loop, the family-activity LATERAL subquery, and the status-aware filtering are well thought through. Test coverage is thorough: 8 subtests covering disabled state, active/inactive, child-activity propagation, status filtering, pinned exemptions, multi-owner audits, idempotency, and batch pagination.

5 reviewers (Test Auditor, Database, Go Architect, Edge Case Analyst, Contract Auditor) ran in parallel. After cross-checking for contradictions and weak evidence, 1 nit survived. Most flagged concerns (authz asymmetry on GetChatAutoArchiveDays, audit coupling in dbpurge, nil-deref on auditor load) are consistent with established codebase patterns (GetChatRetentionDays, autobuild.NewExecutor, ActivateDormantUser).

Comment thread coderd/database/dbpurge/dbpurge.go
@johnstcn johnstcn force-pushed the cj/chat-archive-core branch from 4583b50 to 641ee3a Compare April 23, 2026 08:16
@johnstcn

Copy link
Copy Markdown
Member Author

/coder-agents-review

johnstcn added a commit that referenced this pull request Apr 23, 2026
Layers per-owner digest notifications onto the chat auto-archive
subsystem introduced in #24642.

Each tick's archived rows are grouped by owner, the top 25 titles
per owner are rendered into a new 'Chats Auto-Archived' notification
template, and any remainder surfaces as 'and N more'. Duplicate
same-day digests collapse via the notifier's existing dedupe hash.

The template body branches on retention_days: when retention is
disabled (retention_days=0), users are told archived chats are kept
indefinitely rather than falsely claiming imminent deletion.

Adds one test subtest for the digest overflow cap, plus digest
assertions to the existing archive subtests from #24642. Two
notification test fixtures (standard and retention-zero) lock the
two template branches into goldens.

Depends on #24642. Migration 000476 adds the notification template
row; 000475 (in the core PR) already created the candidate index.

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well-structured PR. The LATERAL family-activity subquery, partial index targeting, and batch-limited purge loop are solid. Test coverage is thorough: 6 subtests with a real Postgres backend, deterministic multi-tick control via quartz.Mock, and proportional batch-size testing. The decision log in the PR description is exemplary.

3 P2, 10 P3, 1 Nit, 2 Notes. 16 reviewers ran in parallel; Ging-Go and Takumi reported no findings (Go idioms and concurrency are clean).

The dominant theme is that the cascade step (c.root_chat_id = t.id) applies no per-child filtering for status or pin_order, creating a gap between the documentation's unqualified guarantees ("Chats whose status indicates ongoing work are never auto-archived", "Pinned conversations are never auto-archived") and the actual behavior. The same cascade shape exists in manual ArchiveChatByID, so this is a consistent convention, not a regression. The simplest fix is likely scoping the doc language to root chats rather than adding complexity to the cascade.

This is a first-round panel review. Findings are the panel's output, not a request for changes.

"Shall I show you? ♥" (Hisoka, on what happens to the audit trail under load)

🤖 This review was automatically generated with Coder Agents.

Comment thread coderd/database/queries/chats.sql
Comment thread coderd/database/dbpurge/dbpurge.go Outdated
Comment thread docs/ai-coder/agents/platform-controls/chat-auto-archive.md Outdated
Comment thread coderd/database/queries/chats.sql
Comment thread coderd/database/queries/chats.sql
Comment thread coderd/database/queries/siteconfig.sql
Comment thread coderd/database/dbpurge/dbpurge.go
Comment thread coderd/database/queries/siteconfig.sql
Comment thread coderd/database/queries/chats.sql
Comment thread coderd/database/dbauthz/dbauthz.go
@johnstcn johnstcn force-pushed the cj/chat-archive-core branch 6 times, most recently from 9e35586 to 1766474 Compare April 23, 2026 16:19
@johnstcn

Copy link
Copy Markdown
Member Author

/coder-agents-review

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Round 2. 12 of 16 R1 findings addressed, 3 acknowledged, 1 contested (DEREM-16 closed by panel vote, 4/4).

All three P2s are fixed: docs scoped to root chats (DEREM-1, DEREM-4), dispatch timeout removed with children skipped from audit (DEREM-2), non-existent Behavior tab reference replaced with API instructions (DEREM-3). The ORDER BY now matches the partial index (DEREM-5). Test coverage expanded with boundary, deleted-message, and all-four-status tests (DEREM-7/8/9). GET auth aligned with retention (DEREM-10).

The DEREM-2 fix (audit roots only, skip children) introduced a doc/code mismatch in the audit section. Two stale comments survived the DEREM-10 fix. 2 P3, 2 Nit new.

"Oh, this is a nice test suite!" (Bisky, on the 10-subtest harness)

🤖 This review was automatically generated with Coder Agents.

Comment thread docs/ai-coder/agents/platform-controls/chat-auto-archive.md Outdated
Comment thread coderd/database/dbpurge/dbpurge_test.go
Comment thread docs/ai-coder/agents/chats-api.md
Comment thread coderd/database/dbauthz/dbauthz.go
@johnstcn johnstcn force-pushed the cj/chat-archive-core branch from 81ffa45 to e1e03ba Compare April 23, 2026 17:41
johnstcn added a commit that referenced this pull request Apr 23, 2026
Layers per-owner digest notifications onto the chat auto-archive
subsystem introduced in #24642.

Each tick's archived rows are grouped by owner, the top 25 titles
per owner are rendered into a new 'Chats Auto-Archived' notification
template, and any remainder surfaces as 'and N more'. Duplicate
same-day digests collapse via the notifier's existing dedupe hash.

The template body branches on retention_days: when retention is
disabled (retention_days=0), users are told archived chats are kept
indefinitely rather than falsely claiming imminent deletion.

Adds one test subtest for the digest overflow cap, plus digest
assertions to the existing archive subtests from #24642. Two
notification test fixtures (standard and retention-zero) lock the
two template branches into goldens.

Depends on #24642. Migration 000476 adds the notification template
row; 000475 (in the core PR) already created the candidate index.
johnstcn added a commit that referenced this pull request Apr 23, 2026
feat(coderd): chat auto-archive owner digest notifications

Layers per-owner digest notifications onto the chat auto-archive
subsystem introduced in #24642.

Each tick's archived rows are grouped by owner, the top 25 titles
per owner are rendered into a new 'Chats Auto-Archived' notification
template, and any remainder surfaces as 'and N more'. Duplicate
same-day digests collapse via the notifier's existing dedupe hash.

The template body branches on retention_days: when retention is
disabled (retention_days=0), users are told archived chats are kept
indefinitely rather than falsely claiming imminent deletion.

Adds one test subtest for the digest overflow cap, plus digest
assertions to the existing archive subtests from #24642. Two
notification test fixtures (standard and retention-zero) lock the
two template branches into goldens.

Depends on #24642. Migration 000476 adds the notification template
row; 000475 (in the core PR) already created the candidate index.
johnstcn added a commit that referenced this pull request Apr 23, 2026
feat(coderd): chat auto-archive owner digest notifications

Layers per-owner digest notifications onto the chat auto-archive
subsystem introduced in #24642.

Each tick's archived rows are grouped by owner, the top 25 titles
per owner are rendered into a new 'Chats Auto-Archived' notification
template, and any remainder surfaces as 'and N more'. Duplicate
same-day digests collapse via the notifier's existing dedupe hash.

The template body branches on retention_days: when retention is
disabled (retention_days=0), users are told archived chats are kept
indefinitely rather than falsely claiming imminent deletion.

Adds one test subtest for the digest overflow cap, plus digest
assertions to the existing archive subtests from #24642. Two
notification test fixtures (standard and retention-zero) lock the
two template branches into goldens.

Depends on #24642.
@johnstcn johnstcn force-pushed the cj/chat-archive-core branch from d0cc93d to ddbfe37 Compare April 23, 2026 19:53
@johnstcn

Copy link
Copy Markdown
Member Author

/coder-agents-review

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Round 3. All 4 R2 findings addressed (e1e03ba). Branch rebased (migration renumbered 000475 -> 000477). Netero R3 clean across all 8 sections.

21 findings across 3 rounds: 17 fixed, 3 accepted, 1 panel-closed. 1 new P3.

Mafuuu and Razor reported no findings. Razor verified every doc claim against the code; all match. The implementation is sound.

🤖 This review was automatically generated with Coder Agents.

Comment thread coderd/database/dbpurge/dbpurge_test.go
johnstcn added a commit that referenced this pull request Apr 23, 2026
Layers per-owner digest notifications onto the chat auto-archive
subsystem introduced in #24642.

Each tick's archived rows are grouped by owner, the top 25 titles
per owner are rendered into a new 'Chats Auto-Archived' notification
template, and any remainder surfaces as 'and N more'. Duplicate
same-day digests collapse via the notifier's existing dedupe hash.

The template body branches on retention_days: when retention is
disabled (retention_days=0), users are told archived chats are kept
indefinitely rather than falsely claiming imminent deletion.

Adds one test subtest for the digest overflow cap, plus digest
assertions to the existing archive subtests from #24642. Two
notification test fixtures (standard and retention-zero) lock the
two template branches into goldens.

Depends on #24642. Migration 000476 adds the notification template
row; 000475 (in the core PR) already created the candidate index.
@johnstcn johnstcn marked this pull request as ready for review April 23, 2026 20:28
@johnstcn

Copy link
Copy Markdown
Member Author

/coder-agents-review

johnstcn added a commit that referenced this pull request Apr 24, 2026
Layers per-owner digest notifications onto the chat auto-archive
subsystem introduced in #24642.

Each tick's archived rows are grouped by owner, the top 25 titles
per owner are rendered into a new 'Chats Auto-Archived' notification
template, and any remainder surfaces as 'and N more'. Duplicate
same-day digests collapse via the notifier's existing dedupe hash.

The template body branches on retention_days: when retention is
disabled (retention_days=0), users are told archived chats are kept
indefinitely rather than falsely claiming imminent deletion.

Adds one test subtest for the digest overflow cap, plus digest
assertions to the existing archive subtests from #24642. Two
notification test fixtures (standard and retention-zero) lock the
two template branches into goldens.

Depends on #24642. Migration 000476 adds the notification template
row; 000475 (in the core PR) already created the candidate index.
Comment on lines +69 to +72
rest of `dbpurge` during the first run. If you need to prevent the
initial backfill entirely during a migration window, set
`auto_archive_days = 0` before upgrading and re-enable it when you
are ready.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is actually not possible. You need to upgrade in order to have this setting. Should we make auto-archive opt-in (i.e. default = 0)?

johnstcn added a commit that referenced this pull request Apr 24, 2026
Layers per-owner digest notifications onto the chat auto-archive
subsystem introduced in #24642.

Each tick's archived rows are grouped by owner, the top 25 titles
per owner are rendered into a new 'Chats Auto-Archived' notification
template, and any remainder surfaces as 'and N more'. Duplicate
same-day digests collapse via the notifier's existing dedupe hash.

The template body branches on retention_days: when retention is
disabled (retention_days=0), users are told archived chats are kept
indefinitely rather than falsely claiming imminent deletion.

Adds one test subtest for the digest overflow cap, plus digest
assertions to the existing archive subtests from #24642. Two
notification test fixtures (standard and retention-zero) lock the
two template branches into goldens.

Depends on #24642. Migration 000476 adds the notification template
row; 000475 (in the core PR) already created the candidate index.

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Round 4. DEREM-21 addressed (23ac3ba). All 21 findings from R1-R3 resolved: 18 fixed, 3 accepted, 1 panel-closed.

Netero R4: clean. Panel (Bisky, Mafuuu, Razor): Mafuuu and Razor reported no findings. Razor verified every policy model claim, SQL query property, and doc assertion against the code. One P4 from Bisky: MultipleOwners asserts audit attribution but not the chats' Archived state; other subtests cover that class of assertion.

22 findings across 4 rounds, 21 resolved. The implementation is thorough and well-tested.

🤖 This review was automatically generated with Coder Agents.

Comment thread coderd/database/dbpurge/export_test.go Outdated
Comment thread coderd/database/dbpurge/export_test.go Outdated
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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a chat message is edited, does that change created_at?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The UpdateChatMessageByID query does not. But editing a chat message in the UI calls the patchChatMessage handler. This calls EditMessage, which soft-deletes the original and all subsequent messages, and then inserts a new message with a fresh created_at. So, yes.

)::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;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doing all of this in a single query is efficient, but I worry about having repeated logic (e.g. archival cascades to children) in multiple places. This is a case where inefficiency might be beneficial (pull target IDs and call regular archival path).

@johnstcn johnstcn Apr 24, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about GetAutoArchivableChatIDs + ArchiveChatsByIDs? Could do it as a separate post-merge followup.

Comment thread coderd/database/queries/siteconfig.sql Outdated
@johnstcn johnstcn merged commit a876287 into main Apr 24, 2026
31 checks passed
@johnstcn johnstcn deleted the cj/chat-archive-core branch April 24, 2026 13:18
@github-actions github-actions Bot locked and limited conversation to collaborators Apr 24, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants