Skip to content

fix(compression): settings tab saves only the fields that changed - #14723

Merged
diegosouzapw merged 9 commits into
cheaper-inference:release/v3.8.51from
woodsonl:fix/compression-settings-tab-partial-save
Sep 29, 2026
Merged

diegosouzapw merged 9 commits into
cheaper-inference:release/v3.8.51from
woodsonl:fix/compression-settings-tab-partial-save

Conversation

@woodsonl

@woodsonl woodsonl commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Summary

The compression settings tab (CompressionSettingsTab.tsx) read the config once on mount, and every save sent that whole snapshot to PUT /api/settings/compression. updateCompressionSettings overwrites every key it receives. So each save from the tab also wrote back the tab's old copy of any setting changed somewhere else since it loaded.

The caveman context page has its own Auto-Clarity checkbox and embeds this tab in its Advanced view. Turn the checkbox off, change any field in the tab, and autoClarity was stored as true again while the checkbox still showed it off. outputStyles saved from another browser tab went back to [] the same way.

The tab now sends only what each save changes, including inside nested objects, and a failed save rolls back on screen with its error left visible.

Related Issues

The Fix

  • save() sends only the keys it is called with, where it used to send { ...config, ...updates }.
  • Nested objects (cavemanConfig, cavemanOutputMode, aggressive, ultra) are stored as one row each, so the PUT still carries each nested object whole. Each call site names only the field it changes, and save() fills in the object's other fields from the server's current row, read just before the PUT. A sibling field changed in the panel or another tab since this tab loaded is not written back. This addresses the review note about cavemanOutputMode.
  • Saves go out one at a time. The form shows the last saved config plus the saves still queued. A failed save drops out of the queue, so only its own fields roll back and a newer save keeps its value.
  • A failed save's error stays on screen until the next edit starts a new save, even when a queued save succeeds after it or an earlier save's 2-second "saved" timeout fires.

Validation

Check Result
New test on the base code (commit 9b51c21) cases 1 to 4 fail
Status assertions on the first fix (head fab0a77) the case 4 status checks and case 5 fail on the missing saveFailed text
Nested-sibling cases before the fix (commit ba74e42 adds only the tests) cases 6 and 7 fail: the tab writes back a stale intensity ("full" for "ultra"), a stale maxTokensPerMessage (2048 for 4096) and a stale moderate threshold (3 for 9)
PR test file at the final head (f8a63cd) 7/7 pass
Compression and caveman UI suites at the final head 20 files, 79/80 pass; the one failure is compressionPanel.test.tsx "derived-pipeline preview reflects the enabled engines" (expects rtk), which never loads the settings tab
Full local vitest UI run at the final head 418 files, 2751/2770 tests pass; the 19 failures are that compressionPanel case plus pool-card, quota-share-delete-error and quota-share-page, whose next-intl mock lacks useLocale; compression-settings-page (2/2) and compression-settings-tab-consolidation (5/5) pass
Dashboard typecheck gate, ESLint and Prettier on the changed files, pre-commit gates pass

These .tsx tests run under vitest.config.ts (npm run test:vitest:ui). CI runs that suite in ci.yml, which triggers for pull requests into main, so the runs above are local; the suite next runs in CI on the release pull request.

Tests Added Or Updated

tests/unit/ui/compression-settings-tab-partial-save.test.tsx (new) runs the tab against a fake route that overwrites each key it gets, as updateCompressionSettings does:

  1. On the caveman page, Auto-Clarity turned off stays off after the embedded tab saves the cache TTL. This case has a 30-second timeout, because rendering the whole page takes over 5 seconds on a cold run.
  2. outputStyles saved from another tab survives a save from this one, and the PUT body is { cacheMinutes: 10 }.
  3. A failed save rolls the field back and shows the error.
  4. A failed save next to a newer successful one rolls back only its own field and keeps the error on screen; the next edit clears it.
  5. An earlier save's 2-second "saved" timeout does not clear a later save's error.
  6. Toggling Auto-Clarity in the tab keeps an output-mode intensity changed elsewhere.
  7. Editing an aggressive threshold keeps a sibling threshold and maxTokensPerMessage changed elsewhere, and the tab then shows the stored row.

Reviewer Notes

  • Saves are serialized with no timeout, so a PUT that never settles holds up the saves queued behind it. The route writes to local SQLite.
  • A save that touches a nested object makes one extra GET first. Nested number inputs save on every keystroke, so typing there sends a GET and a PUT per keystroke. Merging partial nested writes on the server would let the tab send only the changed field and drop that GET.
  • The array fields (compressRoles, skipRules, preservePatterns) are computed from what the tab shows, like any edited field, so an array changed elsewhere is replaced by the edited copy.
  • On the caveman page, the tab's own Auto-Clarity switch shows the value it loaded until a save touches cavemanOutputMode, so after the page checkbox changes the setting the two controls can disagree for a moment.

CompressionSettingsTab loads the compression config once and each save
PUTs that whole snapshot, and updateCompressionSettings overwrites every
key it receives. A setting changed elsewhere after the tab loaded is
written back to its old value by the next save in the tab.

The cases drive the tab against a fake route that overwrites each key it
gets: Auto-Clarity turned off on the caveman page comes back on after
the embedded tab saves the cache TTL, outputStyles saved from another
tab reset to [], and a failed save leaves the new value on screen, alone
or next to a newer save that succeeds. All four fail on this commit.
CompressionSettingsTab loaded the compression config once on mount and
every save sent that whole snapshot. updateCompressionSettings
overwrites each key it receives, so a setting changed elsewhere after
the tab loaded went back to the tab's stale value. On the caveman
context page, turning the Auto-Clarity checkbox off and then editing
any field in the embedded tab turned autoClarity back on while the
checkbox still showed it off. outputStyles saved from another browser
tab reset the same way.

save() now sends only the fields it is called with, as CompressionPanel
does. Saves go out one at a time, and the form shows the last saved
config plus the saves still queued, so a failed save rolls back only its
own fields and leaves a newer save alone. A key the tab changes still
goes out as its whole nested object (cavemanOutputMode, cavemanConfig,
aggressive, ultra) because the server stores each key as one row.
A queued save that succeeded after a failed one replaced the error with
"saved", and an earlier save's 2-second timeout cleared a later error,
so a field that had just rolled back showed no failure. The error now
stays on screen until the next edit starts a new save.

The rollback test now checks the status after a failed save and a
queued success, and a new test covers an earlier success's timeout
firing after a later failure.
@diegosouzapw

Copy link
Copy Markdown
Collaborator

Thanks for this, the partial-save approach and the per-save rollback look right, and the tests fail without the change and pass with it. One follow-up you may want (not blocking): nested objects such as cavemanOutputMode are still sent whole ({ ...config.cavemanOutputMode!, ... }), so editing e.g. its intensity from this tab can still write back a stale nested autoClarity. Sending only the changed nested field, or a test for it, would close that. Also the caveman-page test takes >5s on a cold run, so a longer per-test timeout might avoid flakes under CI load.

The settings tab sends each nested object (cavemanOutputMode, aggressive,
and the rest) whole, built from the copy it loaded, so a save writes back
any sibling field another tab changed since then. Two new cases fail on
the current code: toggling Auto-Clarity writes back a stale output-mode
intensity, and editing an aggressive threshold writes back a stale
sibling threshold and maxTokensPerMessage.

The caveman-page case gets a 30-second timeout, because rendering the
whole page takes over 5 seconds on a cold run.
The settings tab sent each nested object (cavemanConfig, cavemanOutputMode,
aggressive, ultra) whole, built from the copy it loaded, so saving one
field wrote back every sibling field another tab or the panel had changed
since. Toggling Auto-Clarity, for example, wrote back a stale output-mode
level.

Each save now names only the fields it changes. Before the PUT, save()
reads the server's current row for any nested object it touches and fills
in the other fields from that row. The PUT still carries whole objects,
because the server stores each one as a single row, but no stale siblings.
The form still shows the queued edits on top of the last saved state, and
a failed save still rolls back only its own fields.
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