Skip to content

⏳ feat: Add Ephemeral Retention Mode for Forced Temporary Chats - #13811

Open
berry-13 wants to merge 60 commits into
devfrom
admin/retention-mode
Open

⏳ feat: Add Ephemeral Retention Mode for Forced Temporary Chats#13811
berry-13 wants to merge 60 commits into
devfrom
admin/retention-mode

Conversation

@berry-13

@berry-13 berry-13 commented Jun 17, 2026

Copy link
Copy Markdown
Collaborator

Summary

Closes #10052.

Adds an admin option to force Temporary Chat always on for every user.

retentionMode Effect
temporary (default) Only chats a user marks temporary expire
all All chats get an expiration deadline but stay visible until they expire
ephemeral (new) Every newly saved chat is forced temporary: hidden from history and search, auto-expires, and the toggle is locked on

Configuration

interface:
  retentionMode: "ephemeral"
  temporaryChatRetention: 720

What changes

  • Adds RetentionMode.EPHEMERAL and shared helpers for all-data and forced-temporary retention.
  • Forces isTemporary: true and applies the existing temporary-chat expiration behavior in the normal conversation and message save methods.
  • Routes Assistants/Threads messages through the normal message save method so they receive the same retention fields.
  • Applies the all-data retention policy to new file uploads and shared links.
  • Applies retention while creating imports, forks, and duplicates, including the missing route configuration middleware.
  • Forces the Temporary Chat permission while the mode is active.
  • Locks the frontend toggle and keyboard shortcut and shows an accessible administrator-enforced badge.

The default temporary mode and the existing all mode remain unchanged.

Existing data and scope

This PR intentionally does not include a migration, startup sweep, reconciliation pass, or graph-wide cascade.

Enabling ephemeral does not proactively convert pre-existing permanent conversations. A normal conversation/message save applies the policy to the records being saved, while metadata-only operations such as delete, tag rename, project membership, and share updates do not act as conversion triggers. Deployments that require retroactive retention should handle that as a separately designed and operated migration.

This boundary keeps the feature equivalent to “Temporary Chat is always on” instead of turning it into a general-purpose data-retention migration engine.

Concurrency and consistency

This mode reuses the existing Temporary Chat write and TTL behavior. Each MongoDB document write is atomic, but conversation, message, file, and shared-link writes are not one cross-document transaction. This PR does not promise monotonic parent/child deadline capping or graph-wide reconciliation. MongoDB TTL deletion is asynchronous.

Docs: LibreChat-AI/librechat.ai#611

Change Type

  • New feature (non-breaking change which adds functionality)

Testing

Local verification:

  • packages/data-schemas: 52 suites / 1,852 tests passed.
  • packages/data-provider: 25 suites / 1,358 tests passed.
  • packages/api: 275 suites / 7,151 tests passed, with the repository's normal integration-test exclusions.
  • Focused conversation, message, Threads, fork/import, config, file-retention, and permission suites: 398 tests passed.
  • API build and data-provider rollup completed without circular-dependency warnings.
  • Changed files pass Prettier and ESLint; git diff --check is clean.
  • No package manifest or lockfile changes remain.

Test configuration

interface:
  retentionMode: "ephemeral"
  temporaryChatRetention: 1

Start a new chat. The badge is locked on, the saved conversation and messages are temporary with an expiration, and the chat does not appear in history or search. Imports, forks, duplicates, file uploads, and Assistants/Threads messages receive the same policy.

Checklist

  • My code adheres to this project's style guidelines
  • I have performed a self-review of my own code
  • My changes do not introduce new warnings
  • I have written tests demonstrating that my feature works
  • Local unit tests pass with my changes
  • A pull request for updating the documentation has been submitted

@berry-13
berry-13 marked this pull request as draft June 17, 2026 13:10

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3f6757b041

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread api/server/utils/import/importBatchBuilder.js
@berry-13

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2de5b1f164

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread api/server/routes/convos.js
@berry-13
berry-13 marked this pull request as ready for review June 17, 2026 23:43

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5481e9a498

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/data-schemas/src/methods/conversation.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

{ userId: req.user.id },

P2 Badge Pass retention config when pinning conversations

When interface.retentionMode: "ephemeral" is enabled and a user pins or unpins an older non-temporary conversation, this route still calls saveConvo with only userId, unlike the archive/update routes above. Since saveConvo only forces isTemporary/expiredAt when it receives interfaceConfig, the pin write preserves the conversation as permanent and visible, bypassing the forced-temporary policy. Add configMiddleware here and pass req.config.interfaceConfig into saveConvo.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@berry-13
berry-13 changed the base branch from main to dev June 18, 2026 08:12

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7b4b36de6f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/api/src/files/retention.ts
Comment thread packages/data-schemas/src/methods/message.ts Outdated
Repository owner deleted a comment from chatgpt-codex-connector Bot Jun 20, 2026
@berry-13

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Hooray!

Reviewed commit: f63be383c8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@berry-13
berry-13 force-pushed the admin/retention-mode branch from f63be38 to 596c5b4 Compare June 20, 2026 23:34

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 596c5b4118

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/data-schemas/src/methods/conversation.ts
Comment thread packages/data-schemas/src/methods/message.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4b170f5ff3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/data-schemas/src/methods/message.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 144ad1e5b3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/data-schemas/src/methods/message.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f91cbfeb66

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/data-schemas/src/methods/message.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1cb50554df

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/data-schemas/src/methods/message.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3378917bdf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread api/server/routes/agents/index.js Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5dfae72636

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/data-schemas/src/methods/message.ts
Comment thread packages/data-schemas/src/methods/message.ts Outdated
@berry-13

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 18119a74ec

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/data-schemas/src/utils/retention.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e798bc5919

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/data-schemas/src/methods/message.ts Outdated
Comment thread api/server/routes/messages.js Outdated
@berry-13
berry-13 force-pushed the admin/retention-mode branch from e798bc5 to 94642c8 Compare June 22, 2026 21:55

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 94642c8cd2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/data-schemas/src/methods/conversation.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f1bfb90c4e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread client/src/components/Chat/TemporaryChat.tsx

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 920814a3d4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/data-provider/src/config.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c4aa8ae7ab

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread api/server/services/Threads/manage.js
@berry-13
berry-13 force-pushed the admin/retention-mode branch from c4aa8ae to b960221 Compare June 23, 2026 14:42
@berry-13

Copy link
Copy Markdown
Collaborator Author

@codex review

applyForcedRetention (message edit/feedback/share/tag writes) converts a chat to
isTemporary: true, which visibleProjectConversationFilter excludes, but unlike
saveConvo it never recomputed the owning project's cached stats. A pre-existing
project chat converted this way left the project's conversationCount and
lastConversationId pointing at a chat the workspace no longer shows.

After the cascade, recompute the stats of every touched chat's project (the
single conversation for applyForcedRetention, all tagged chats for
applyForcedRetentionToTag), matching saveConvo's retention-visibility refresh.
Scoped to conversations carrying a chatProjectId, so it is a no-op for chats
outside any project.
saveConvo's inline forced-retention backfill converted the conversation and
capped its messages and shared links but left File records at expiredAt: null,
so archive/pin/title updates on a pre-existing chat under ephemeral mode left
its uploads in storage after the conversation and messages TTL out. File cleanup
only sweeps rows whose own expiredAt is set.

Cap the conversation's files alongside messages and shares, matching the other
forced-retention cascades.
cascadeForcedConversationRetention converted the parent conversation first and
only backfilled its messages, shares, and files when that update modified a row.
If a child backfill threw after the parent was converted, a later forced-retention
write saw the parent already conforming (modifiedCount 0) and skipped the
children, leaving not-yet-updated messages, shares, or files permanent.

Gate on an in-memory conversationNeedsForcedRetention check and backfill the
children before marking the parent conforming, so a failed child leaves the
conversation non-conforming and the next write re-runs the whole cascade. The
early return preserves the no-op-when-already-conforming behavior.
Opening a pre-existing permanent conversation under ephemeral retention only
flips the composer isTemporary atom; the loaded conversation object stays
non-temporary until a write converts it server-side. BookmarkMenu gated solely
on isTemporaryConversation(conversation), so it kept exposing bookmarking on a
chat that is already effectively temporary.

Gate the menu on the forced-retention flag as well (matching how the composer
already treats the chat), so permanent-chat actions are hidden until the server
catches up.
The migration loaded getAppConfig() with no tenant and swept every conversation
under runAsSystem, so in a multi-tenant deployment with per-tenant retention
config overrides it force-expired all tenants' chats using the system config's
window, even tenants that never enabled ephemeral retention.

Enumerate tenants from the conversations, and for each run getAppConfig and the
sweep inside tenantStorage.run({ tenantId }) so every query is scoped to that
tenant; skip a tenant whose resolved config is not ephemeral. Single-tenant and
pre-tenancy deployments (no tenantId) keep the existing system-config sweep, and
untenanted rows in a mixed deployment are left untouched (they cannot be scoped)
with a warning rather than converted cross-tenant.
saveConvo's findOneAndUpdate wrote isTemporary/expiredAt on the conversation
before the message/share/file backfills ran, so a child failure left the parent
conforming; the next save reloaded it, conversationNeedsForcedRetention returned
false, and the skipped child rows stayed permanent even though retries appeared
to succeed.

Run the child backfills before the parent conversion, matching the cascade and
sweep ordering, so a failed child leaves the conversation non-conforming and the
next save re-runs the whole backfill.
…ect chats

The migration sweep converted project-assigned conversations to
isTemporary: true, which visibleProjectConversationFilter excludes, but never
recomputed the owning projects' cached stats, leaving conversationCount and
lastConversationId pointing at chats the project view no longer shows.

Collect each converted conversation's project membership in the sweep (deduped
user/chatProjectId pairs returned alongside the counts; the sweep cannot refresh
directly without a circular dependency on the chat-project methods) and have the
migration recompute every affected project's stats inside the tenant context,
reporting the refreshed count.
The migration used distinct('tenantId') to detect untenanted rows, but distinct
only enumerates stored field values: pre-tenancy conversations missing the field
entirely contribute nothing to it, so in a mixed deployment they were silently
left permanent with no skippedUntenanted warning.

Count untenanted rows directly with countDocuments({ tenantId: null }) (which
matches both explicit null and missing fields, plus empty strings via $or) so
the warning fires whenever unscopeable rows exist, and report how many.
Both the migration sweep and the convert-on-touch cascade skipped a parent that
already satisfied the forced-retention gap filter, so an already-temporary chat
with dependent rows that lag behind it (permanent shares or later-window files
created before the mode switch, or left by a partial earlier backfill) kept
those children past the parent's TTL.

Cap the children independently of the parent gap check: the single cascade runs
the message/share/file caps before its parent conversion regardless of whether
the parent needs converting (each cap is an indexed no-op once children
conform; the bulk tag/project cascade already worked this way), and the sweep
gains an alignment pass that walks already-conforming temporary conversations
and caps their children to each parent's own deadline, reported via a new
`aligned` count. The cap helpers now return their modified counts to support
that reporting.
The migration evaluates one config per tenant, but runtime requests resolve
config with role/user principals, so a deployment using principal-scoped DB
config overrides can have users whose effective retention mode or window
differs from the tenant default. Sweeping such a tenant with the tenant-level
config would skip users who are ephemeral, or worse, force-expire chats of
users whose override opts out.

Detect active non-base config overrides that touch interface.retentionMode or
interface.temporaryChatRetention (including tombstones removing them) and skip
the tenant with an explanatory error listing the principals; --force sweeps
with the tenant-level config anyway. Skip reasons now surface in the CLI
output.
saveConvo only ran the message/share/file caps when the touched conversation
itself needed forced conversion, so an archive/pin/title save on an
already-temporary chat with an earlier deadline skipped the child caps and any
lagging rows from before the mode switch or a partial backfill kept
expiredAt: null or a later deadline past the parent's TTL.

Run the child caps whenever forced retention resolves an active deadline for a
pre-existing conversation, matching the single cascade's self-healing behavior;
each cap is an indexed no-op once the chat's children conform. The old
"leaves messages untouched once ephemeral" expectation pinned the skip this
change removes, so it now asserts the permanent row is healed instead.
saveMessage's forced-retention cascade converts an old project-assigned chat to
isTemporary: true, but message-only writes (branch/artifact/abort saves) have no
saveConvo afterward to recompute the project's cached stats, so the count and
lastConversationId kept pointing at a chat the project view no longer shows.

Make cascadeForcedConversationRetention report whether it converted the parent
row and refresh the owning project's stats from saveMessage when it did. The
applyForcedRetention refresh is gated on the same signal, so conforming parents
no longer trigger a redundant stats recompute on every edit/feedback write.
Message-attachment uploads create File rows without a conversationId — they are
referenced only from Message.files[].file_id and the conversation's files array
(only Code Interpreter outputs set conversationId). The conversation-scoped file
caps therefore missed regular attachments entirely: converting a pre-existing
chat left their expiredAt null, so storage cleanup never swept them after the
conversation and messages expired.

Collect the referenced file ids (conversation.files plus one pass over the
chat's messages) and extend the file caps to match file_id as well as
conversationId, in the single cascade, saveConvo, the bulk tag/project cascade,
and both sweep passes. The id scan runs at conversion/alignment time only —
post-conversion uploads always receive a deadline at upload, so conforming-
parent writes keep the cheap conversationId-scoped cap. A file shared across
chats is capped to the earliest converting chat's deadline, consistent with
cap-don't-extend.
The share route enforced retention after createSharedLink and the permission
grant. If the cascade threw at that point, the client got a 500 with a live
share already created, and a retry never reached the cascade again because
createSharedLink rejects when an active share exists — leaving a valid share
whose source conversation stays non-temporary and non-expiring under ephemeral
mode.

Convert the source conversation first: a failed share attempt (or an
existing-share retry) still converts the touched chat, and the share expiration
resolution now reads the converted conversation's deadline.
PUT /api/tags/:tag renamed the tag first and ran the forced-retention cascade
with the new tag afterwards. A cascade failure returned 500 with the rename
already committed, and a retried PUT /:oldTag hit the 404 path because the old
tag no longer exists, so the tagged conversations' messages, shares, and files
could never be backfilled.

Run the cascade with the old tag before the rename, mirroring the DELETE route:
the old tag selects the same conversations the renamed tag will carry,
enforcing on a nonexistent tag is a no-op, and a failed rename now retries
cleanly with retention already applied.
collectConversationFileIds only read Message.files, but agent/tool outputs
(code artifacts, generated images) are stored on Message.attachments with their
own file_id references, and those File rows also commonly lack a
conversationId. A forced-retention conversion or migration could therefore
convert the conversation and messages while leaving attachment file metadata at
expiredAt: null, so storage cleanup never swept it.

Match messages carrying either field and collect file ids from both arrays.
Two gaps in the referenced-file-id capping:

The file_id branch matched File rows with no ownership check, and file ids
inside message/conversation documents are caller-supplied (saveMessage spreads
request params into the update). A crafted or imported message referencing
another user's file_id would therefore set that user's File.expiredAt during a
forced-retention cascade or migration, and the storage sweeper would later
delete their object. The referenced-id branch now always filters on File.user
(dropped entirely when the caller's user id is not a castable ObjectId string —
fail closed), in both capConversationFiles and the bulk tag/project scope. The
conversationId branch stays unscoped: those rows are server-created by the
conversation's own processes, and an id collision can only shorten the
colliding owner's files.

Assistants conversations persist thread uploads on Conversation.file_ids
(saveUserMessage/syncMessages), which the parent projections never read, so
files stored only there kept expiredAt: null after conversion. A shared
conversationSeedFileIds helper now seeds the id collection from both files and
file_ids across the cascade, saveConvo, the bulk cascade, and both sweep
passes.
Regular message edits now load app config and stamp ephemeral
retention on the message and conversation. Bookmark controls also
hide when temporary mode is forced on a still-permanent chat.
A text edit that omits `index` reached `updateMessage`, which matches on
`{ messageId, user }` alone, so the route's conversationId could be paired
with a message from another of the caller's chats and the retention re-stamp
would move it there. Reject the mismatch instead.

Artifact edits stamped forced retention onto the message but never onto its
conversation, leaving the chat permanently visible while its edited message
expired. Both edit routes now share one parent-retention helper, which never
upserts a conversation that is already gone.
Ephemeral retention marks imported, forked, and duplicated chats temporary,
which excludes them from every bookmark-filtered conversation query, and TTL
deletion never decrements tag counts. Counting their tags therefore left
permanent totals for chats that can never appear under the filter.
Rebasing onto dev left duplicate and dead imports in the data-schemas specs,
and the earlier reduction of ephemeral retention reverted createFile to the
plain upsert without dropping the two tests that asserted its pipeline-only
casting and monotonic expiredAt. Restore the imports the surviving tests need
and remove the tests for behavior the method no longer has.
@berry-13
berry-13 force-pushed the admin/retention-mode branch from 01dfa71 to 9438549 Compare August 17, 2026 18:46

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9438549980

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

* @returns {TMessage} The newly created branch message
*/
router.post('/branch', async (req, res) => {
router.post('/branch', configMiddleware, async (req, res) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Restamp the parent when creating branch messages

When ephemeral mode is enabled after permanent conversations already exist, this middleware makes saveMessage mark the new branch message temporary, but the route never applies retention to sourceMessage.conversationId. Creating a parallel-response branch can therefore leave the parent conversation permanent and visible while its newly created branch expires; call the same parent-retention helper used by the artifact and edit routes after saving the branch.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Enhancement]: Temporary Chat always on

2 participants