From 7e0c9f526a63006acddc042e7d42acb98e2df7f3 Mon Sep 17 00:00:00 2001 From: Aaron Scherer <896295+cryptiklemur@users.noreply.github.com> Date: Thu, 17 Sep 2026 14:24:21 -0500 Subject: [PATCH] feat(sse): allow disabling conversation tracking (#13150) * feat(sse): allow disabling conversation tracking * docs: document OMNIROUTE_DISABLE_CONVERSATION_TRACKING --- .env.example | 9 ++++++ .../13150-disable-conversation-tracking.md | 1 + docs/reference/ENVIRONMENT.md | 1 + open-sse/services/conversationTracker.ts | 6 +++- tests/unit/conversationTracker.test.ts | 31 +++++++++++++++++++ 5 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 changelog.d/features/13150-disable-conversation-tracking.md diff --git a/.env.example b/.env.example index 2aa3eee4ef..c643e3c467 100644 --- a/.env.example +++ b/.env.example @@ -622,6 +622,15 @@ ALLOW_API_KEY_REVEAL=false # Validated to >= 1, clamped to <= 32. | Default: 3 # COMBO_CONCURRENCY_PER_MODEL=3 +# Disable conversation-history tracking (#13150). +# Used by: open-sse/services/conversationTracker.ts. resolveConversationId() +# returns an untracked result before it reads SQLite or parses message history, +# and the switch also covers client-supplied session IDs. Routing sessions are +# unaffected and existing records are not deleted. Use it when the dashboard's +# conversation view is unused and the turn table has grown large. +# Set to 1 to disable. | Default: unset (tracking enabled) +# OMNIROUTE_DISABLE_CONVERSATION_TRACKING=1 + # ═══════════════════════════════════════════════════════════════════════════════ # 7. URLS & CLOUD SYNC # ═══════════════════════════════════════════════════════════════════════════════ diff --git a/changelog.d/features/13150-disable-conversation-tracking.md b/changelog.d/features/13150-disable-conversation-tracking.md new file mode 100644 index 0000000000..a31d590c13 --- /dev/null +++ b/changelog.d/features/13150-disable-conversation-tracking.md @@ -0,0 +1 @@ +- **feat(sse):** `OMNIROUTE_DISABLE_CONVERSATION_TRACKING=1` turns off conversation-history collection for operators who do not use the dashboard's conversation view. `resolveConversationId()` returns an untracked result before it reads SQLite or parses message history, and the switch also covers client-supplied session IDs. Routing-session handling is unchanged, tracking stays on by default, and existing records are not deleted. One reporting install held 5.97 million turn records at about 4.26 GB ([#13150](https://github.com/diegosouzapw/OmniRoute/pull/13150)) diff --git a/docs/reference/ENVIRONMENT.md b/docs/reference/ENVIRONMENT.md index 71b6578f3c..d91a92cfbf 100644 --- a/docs/reference/ENVIRONMENT.md +++ b/docs/reference/ENVIRONMENT.md @@ -313,6 +313,7 @@ OmniRoute provides a two-layer defense: request-side injection scanning and resp | `OMNIROUTE_PREFER_CLAUDE_CODE_FOR_UNPREFIXED_CLAUDE_MODELS` | `false` | `open-sse/services/model.ts` | Opt-in: route bare `claude-*` model IDs from Claude Code clients through the Claude Code OAuth account instead of requiring a provider prefix. Explicit provider prefixes still win. Also configurable via a dashboard toggle on the Claude provider page. | | `COMBO_CONCURRENCY_PER_MODEL` | `3` | `open-sse/services/comboConfig.ts` | Per-model concurrency cap for round-robin combos (#9100). The round-robin combo semaphore was hard-capped at 3 concurrent requests per model with no override, serializing higher-concurrency traffic behind that cap. Validated to `>= 1`, clamped to `<= 32`. | | `DISABLE_CONTEXT_WINDOW_CHECKS` | `false` | `open-sse/handlers/chatCore.ts` | Dangerous opt-in that skips OmniRoute's local context-window / max-input-token check for direct single-model requests. Upstream providers still enforce their actual limits; prompt compression and the model's own output-token cap remain active. Effective precedence is Feature Flags DB override > environment variable > default; no restart is required. | +| `OMNIROUTE_DISABLE_CONVERSATION_TRACKING` | _(unset)_ | `open-sse/services/conversationTracker.ts` | Set `1` to stop collecting conversation history. `resolveConversationId()` returns an untracked result before it reads SQLite or parses message history, and client-supplied session IDs are covered too. Routing-session handling is unchanged and existing records are not deleted. For deployments that do not use the dashboard's conversation view and want the turn table to stop growing. | --- diff --git a/open-sse/services/conversationTracker.ts b/open-sse/services/conversationTracker.ts index e24489a000..3d19987f10 100644 --- a/open-sse/services/conversationTracker.ts +++ b/open-sse/services/conversationTracker.ts @@ -77,7 +77,7 @@ export interface ResolveConversationIdInput { } export interface ResolveConversationIdResult { - conversationId: string; + conversationId: string | null; isNewConversation: boolean; } @@ -459,6 +459,10 @@ const MAX_STORED_ID_LENGTH = 128; export async function resolveConversationId( input: ResolveConversationIdInput ): Promise { + if (process.env.OMNIROUTE_DISABLE_CONVERSATION_TRACKING === "1") { + return { conversationId: null, isNewConversation: false }; + } + // Client override wins outright — deterministic, zero heuristic risk. // Same header feature #8249 already reads (chatCore.ts); we don't invent a // new prefix so the existing header's contract/format stays unchanged. diff --git a/tests/unit/conversationTracker.test.ts b/tests/unit/conversationTracker.test.ts index fb61360445..793a49b2ce 100644 --- a/tests/unit/conversationTracker.test.ts +++ b/tests/unit/conversationTracker.test.ts @@ -144,6 +144,37 @@ test("computeFingerprintHash: identical apiKeyId/model/toolNames produce the sam assert.equal(a, b); }); +test("resolveConversationId: disabled tracking skips database access and message parsing", async () => { + const { getDbInstance } = await import("../../src/lib/db/core.ts"); + const db = getDbInstance(); + const previous = process.env.OMNIROUTE_DISABLE_CONVERSATION_TRACKING; + const prepare = test.mock.method(db, "prepare", () => { + throw new Error("disabled tracking must not access SQLite"); + }); + process.env.OMNIROUTE_DISABLE_CONVERSATION_TRACKING = "1"; + try { + for (const clientSessionIdHeader of [null, "explicit-session"]) { + const result = await resolveConversationId({ + body: { + get messages() { + throw new Error("disabled tracking must not parse messages"); + }, + }, + model: "test-model", + apiKeyId: "test-key", + clientSessionIdHeader, + correlationId: "disabled-tracking", + }); + assert.deepEqual(result, { conversationId: null, isNewConversation: false }); + } + assert.equal(prepare.mock.callCount(), 0); + } finally { + prepare.mock.restore(); + if (previous === undefined) delete process.env.OMNIROUTE_DISABLE_CONVERSATION_TRACKING; + else process.env.OMNIROUTE_DISABLE_CONVERSATION_TRACKING = previous; + } +}); + test("resolveConversationId: exact-match continuation reuses the same id", async () => { const apiKeyId = "key-exact"; const turn1 = await resolveConversationId({