Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""add incognito_record_mode to chat_session

Revision ID: 0ec213a5ffde
Revises: a44c4ebac3d6
Create Date: 2026-08-07 13:05:59.971436

"""

import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision = "0ec213a5ffde"
down_revision = "a44c4ebac3d6"
branch_labels = None
depends_on = None


def upgrade() -> None:
# Unbounded VARCHAR, as reasoning_effort_override on this table already
# does, so adding a longer mode stays a code change.
op.add_column(
"chat_session",
sa.Column("incognito_record_mode", sa.String(), nullable=True),
)


def downgrade() -> None:
op.drop_column("chat_session", "incognito_record_mode")
68 changes: 68 additions & 0 deletions backend/onyx/db/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -712,3 +712,71 @@ class SSOProviderType(str, PyEnum):
GOOGLE_OAUTH = "GOOGLE_OAUTH"
OIDC = "OIDC"
SAML = "SAML"


class IncognitoRecordMode(str, PyEnum):
"""What a workspace retains from an incognito chat.

Both modes keep the chat out of the owner's own history and refuse memory
writes. The mode governs what else the workspace may record. Every mode
meters usage, since token rate limits read those rows and incognito must
never become a quota-evasion route. Feature-off is deliberately not a
member: disabling incognito is an admin setting, never a mode pinned on a
session.

Values differ from member names here, so the column storing this passes
``values_callable`` to persist the value rather than the default name.
"""

# Content persists as an ordinary chat, hidden only from the owner's own
# surfaces (history, search, project lists).
FULL_HISTORY = "full_history"
# No conversation content is written to Postgres. Usage is still metered.
USAGE_ONLY = "usage_only"

@classmethod
def from_context_value(cls, value: str | None) -> "IncognitoRecordMode | None":
"""None outside incognito. Unknown values fail closed to USAGE_ONLY."""
if value is None:
return None
try:
return cls(value)
except ValueError:
return cls.USAGE_ONLY

@property
def persists_content(self) -> bool:
"""Whether conversation content may be written to chat_message rows.

Content-free modes still write the rows, with empty text and real
token counts. False means content is never written, not
written-and-hidden: deletion is not atomic across WAL, replicas,
and backups.
"""
return self is IncognitoRecordMode.FULL_HISTORY

@property
def emits_external_traces(self) -> bool:
"""Whether spans may reach external trace processors.

Redacting a payload still sends a request to a destination the trust
boundary denies, so suppression is total rather than scrubbed. Internal
spans always run: the usage ledger is a tracing processor consuming
them, so metering needs no egress.
"""
return self is IncognitoRecordMode.FULL_HISTORY

@property
def fires_hooks(self) -> bool:
"""Whether the query-processing hook may run.

The hook ships the raw query and the user's email to a customer-
configured endpoint before persistence, which the trust boundary denies
for anything but a fully-recorded chat.
"""
return self is IncognitoRecordMode.FULL_HISTORY


def record_mode_persists_content(mode: IncognitoRecordMode | None) -> bool:
"""None is an ordinary chat, which always persists content."""
return mode is None or mode.persists_content
13 changes: 13 additions & 0 deletions backend/onyx/db/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
HierarchyNodeType,
HookFailStrategy,
HookPoint,
IncognitoRecordMode,
IndexingMode,
IndexingStatus,
IndexModelStatus,
Expand Down Expand Up @@ -3067,6 +3068,18 @@ class ChatSession(Base):
description: Mapped[str | None] = mapped_column(Text, nullable=True)
# This chat created by OnyxBot
onyxbot_flow: Mapped[bool] = mapped_column(Boolean, default=False)
# Pinned at creation, so a later setting change cannot alter a live
# session. NULL is an ordinary chat and records normally. Of the incognito
# modes only FULL_HISTORY writes conversation content into messages.
incognito_record_mode: Mapped[IncognitoRecordMode | None] = mapped_column(
Enum(
IncognitoRecordMode,
native_enum=False,
values_callable=lambda x: [e.value for e in x],
),
nullable=True,
default=None,
)
# Only ever set to True if system is set to not hard-delete chats
deleted: Mapped[bool] = mapped_column(Boolean, default=False)
# controls whether or not this conversation is viewable by others
Expand Down
Loading