Skip to content

fix(db): merge partial cavemanOutputMode writes with the stored row - #15206

Merged
diegosouzapw merged 3 commits into
diegosouzapw:release/v3.8.52from
woodsonl:fix/caveman-output-mode-partial-write
Oct 2, 2026
Merged

diegosouzapw merged 3 commits into
diegosouzapw:release/v3.8.52from
woodsonl:fix/caveman-output-mode-partial-write

Conversation

@woodsonl

@woodsonl woodsonl commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Summary

A partial cavemanOutputMode write now keeps the fields it leaves out. updateCompressionSettings merges what the caller sends with the stored row, inside the same transaction, the way it already merges lite.

Since #14551 merged on September 24, 2026, the output-styles path reads autoClarity, so a write that resets it to true turns the content bypass back on for chat requests. Resetting enabled turns the legacy caveman output mode off, and resetting intensity drops it to lite.

Any API client that sends part of the object hits this, through PUT /api/settings/compression or PUT /api/context/caveman/config, which re-exports the same handler (context/caveman/config/route.ts:1). The dashboard pages and the MCP tool send all three fields.

The Defect

On the merge base, updateCompressionSettings wrote every key except engines and lite with insert.run(NAMESPACE, key, JSON.stringify(value)) (src/lib/db/compression.ts:931). The request schema makes all three cavemanOutputMode fields optional (cavemanOutputModeSchema, compressionConfigSchemas.ts:43-49), so a partial object reached the row as sent. On read, normalizeCavemanOutputModeConfig fills each missing field from DEFAULT_CAVEMAN_OUTPUT_MODE_CONFIG: enabled: false, intensity: "lite", autoClarity: true (open-sse/services/compression/types.ts:463-467).

Two writes show it:

  • Stored { enabled: true, intensity: "ultra", autoClarity: true }, then {"cavemanOutputMode":{"autoClarity":false}}: reads back { enabled: false, intensity: "lite", autoClarity: false }.
  • Stored autoClarity: false, then {"cavemanOutputMode":{"enabled":true}}: reads back autoClarity: true and intensity: "lite".

What that changes at runtime:

  • resolveOutputStyleSelection maps enabled and intensity to the terse-prose style when no Output Styles are picked (outputStyles/backCompat.ts:25-26). The first write switches that style off.
  • On the release/v3.8.52 tip, chatCore passes config.cavemanOutputMode?.autoClarity to applyOutputStyles (chatCore.ts:1708), which runs the content bypass unless the value is false (apply.ts:206). The second write switches the bypass back on.

The Fix

  • normalizeCavemanOutputModeConfig takes a fallback (default DEFAULT_CAVEMAN_OUTPUT_MODE_CONFIG) and uses it field by field (compression.ts:117-131).
  • mergeCavemanOutputModeForWrite (compression.ts:566-580) reads the stored row and normalizes the incoming value with that row as the fallback.
  • The write loop sends cavemanOutputMode through it (compression.ts:944-947), inside the same db.transaction as the insert.

Edge cases:

  • An empty {} writes the stored values back unchanged.
  • A field with an invalid value keeps the stored value. The route's strict schema answers such a body with a 400 first, so this rule covers code that calls updateCompressionSettings directly.
  • A non-text (BLOB) stored row counts as absent, the same rule getCompressionSettings applies on read (compression.ts:711-723).

After a partial write, the row holds all three fields. The omitted ones keep what a read returned before the write: the stored value, or the default when the row had none. A field added to CavemanOutputModeConfig (types.ts:78-82) needs a line in the normalizer too, or writes drop it.

Related Work

  • fix(compression): settings tab saves only the fields that changed #14723 makes the settings tab send only the fields that changed. The tab used to PUT the snapshot it loaded on mount, so saving any field there reverted an autoClarity changed elsewhere. A key the tab changes still goes out as its whole nested object; with this PR, a client can send just the nested field it changes.
  • The MCP tool builds its write from getCompressionSettings() and spreads all three fields (compressionTools.ts:432, :464-469). That read caches for 5 s per process (compression.ts:62, :902-907), so an MCP server running as its own process under omniroute --mcp can write back values up to 5 s old.

One Question for You

Nine more keys reset the same way. The write loop stores them whole, their normalizers fill omitted fields with defaults, and their schemas accept partial objects: cavemanConfig, rtkConfig, codexResponsesConfig, languageConfig, aggressive, ultra, sessionDedup, ccr, and contextBudget. headroom and contextEditing hold one field each, so only an empty {} resets them. The MCP tool and /api/context/rtk/config merge before they write (compressionTools.ts:453-462, context/rtk/config/route.ts:29-31), but a partial PUT /api/settings/compression resets the fields it leaves out. Should I extend the merge to these keys in a follow-up PR?

Testing

With the merge-base compression.ts swapped back in and the new tests kept, 6 of the 7 new cases fail: the route case and 5 of the 6 DB cases. The seventh, "treats a non-text, unparseable, or null stored row as absent", passes there because the base code never reads the stored row; it guards the new read.

Run the two files with:

DISABLE_SQLITE_AUTO_BACKUP=true node --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts \
  --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit \
  tests/unit/compression/db.test.ts tests/unit/api/compression/compression-api.test.ts

On this branch:

Check Result
The two changed test files 32/32 pass
Compression lane: tests/unit/compression/**, the compression and combos-default route tests, chatcore-compression-integration, test-model-compression-off-6240 1,614 tests: 1,599 pass, 1 skipped, 14 fail. The same 14 fail on the merge base (1,607 tests there, 1,592 pass).
npm run test:vitest 482/482 in 51 files
ESLint on the three changed files 0 errors, 0 warnings. Full npm run lint exits 2 on stale suppressions in files outside this change, as on the base; CI's npm run lint:json passes them through.
tsc on compression.ts and db.test.ts clean. npm run typecheck:core skips compression.ts; its one error, src/lib/services/cliproxyAccountHealth.ts:157, comes from the merge base and is fixed upstream.
Live checks on an isolated instance both reported writes keep the omitted fields; {} leaves the row as stored; {"intensity":"bogus"} gets a 400; the dashboard Auto-Clarity checkbox saves and survives a reload; a server restart keeps the merged row
Generated merge cases (one-off harness, outside this PR) 15,315 cases plus rollback and same-process queued partial-write checks, run on the writer, merge, and normalizer extracted from compression.ts against in-memory SQLite: all pass

These runs used the merge base d7be9fd528 on release/v3.8.51, which predates the #14551 merge. The branch now includes release/v3.8.52 at dbe703a000, merged with no conflicts, and the two changed test files pass 32/32 on the merged tree. The live checks ran on 793fb5b7ed, whose production code matches the head; they differ only in the two test files.

The 14 lane failures, all inherited from the merge base:

Upstream has since changed every file these tests live in: #14423 rewrote the 5 messages[0] assertions to expect a trailing system message, #14511 (which drains the #14496 base-reds) changed the five plan-test files, and #14551 also edited chatcore-compression-integration.test.ts.

CI on this PR: the first run failed Docs Gates and the docs-counts test in unit shard 3/4 (check-docs-counts-sync.test.ts) on stale 3.8.51 version strings in README.md and llm.txt (#15100, since closed). Those strings read 3.8.52 at dbe703a000, and the docs-counts gate passes on the merged tree. Fast Quality Gates fails its mutation-test-coverage gate here and on #15203, a sibling PR on the same base, because 25 upstream unit tests across 8 modules are missing from stryker.conf.json; all 8 modules sit outside this PR's diff.

updateCompressionSettings stored each cavemanOutputMode write as the
whole JSON row, and getCompressionSettings fills missing fields with
defaults, so a partial write reset every field it left out.
{"autoClarity":false} also turned legacy output mode off and dropped
intensity to "lite". {"enabled":true} reset a stored autoClarity false
to true and intensity to "lite". The PUT /api/settings/compression
schema makes all three fields optional, so the route accepts both
bodies as-is.

The write path now reads the stored row inside the same transaction and
takes each missing or invalid field from it, the way
mergeLiteSettingsForWrite handles lite. An empty {} write leaves the
row unchanged. Like getCompressionSettings, the merge treats a non-text
(BLOB) stored row as absent.

Six regression cases in tests/unit/compression/db.test.ts cover both
reported writes, fields set back to their defaults, the empty write,
invalid fields, a partially populated stored row, and non-text,
unparseable, or null stored rows. A route test in
tests/unit/api/compression/compression-api.test.ts sends the reported
partial PUT body.
@diegosouzapw
diegosouzapw merged commit 1caa0e1 into diegosouzapw:release/v3.8.52 Oct 2, 2026
15 of 16 checks passed
@diegosouzapw

Copy link
Copy Markdown
Owner

Thanks @woodsonl — merged into release/v3.8.52; it ships in the next release.

woodsonl added a commit to woodsonl/OmniRoute that referenced this pull request Oct 4, 2026
…ish pack

Since diegosouzapw#15206 the store merges a partial cavemanOutputMode into its row, so the
fake server does the same, and the Auto-Clarity case now requires the page to
send only { autoClarity }. Add a case that turns a pack off and keeps English,
and make the failed-read case a language save that starts from a failed first
read.
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.

2 participants