Skip to content

fix(desktop): bound numeric config fields so compression ratios can't go out of range (fixes #65703) - #65748

Open
hansai-art wants to merge 1 commit into
NousResearch:mainfrom
hansai-art:fix/desktop-compression-slider-bounds
Open

fix(desktop): bound numeric config fields so compression ratios can't go out of range (fixes #65703)#65748
hansai-art wants to merge 1 commit into
NousResearch:mainfrom
hansai-art:fix/desktop-compression-slider-bounds

Conversation

@hansai-art

@hansai-art hansai-art commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

The desktop settings render every type: 'number' config field through one generic <Input type="number"> in config-settings.tsx that carries no min/max/step. So compression.threshold and compression.target_ratio accept negatives and values above their valid range, exactly as reported.

Repro (code-level, addressing needs-repro): on current main the number branch is

<Input type="number" onChange={e => { const n = raw === '' ? 0 : Number(raw); if (!Number.isNaN(n)) onChange(n) }} .../>

(apps/desktop/src/app/settings/config-settings.tsx:154-171) — no bound anywhere, so -0.5 / 1.5 flow straight into config for any numeric field.

The fix declares bounds per field, applies them to the input, and clamps in onChange. That second half is what actually enforces the bound: a bare min/max on an HTML number input only constrains the spinner arrows and native validation, so a user can still type -5 or 1.5.

Bounds mirror the runtime contract, not the nominal [0, 1] of a ratio:

  • compression.target_ratio0.10-0.80, matching the runtime clamp max(0.10, min(summary_target_ratio, 0.80)) at agent/context_compressor.py:1331 and the documented range at website/docs/developer-guide/context-compression-and-caching.md:104. Saving 0.05 from the UI would otherwise be silently normalized away by the runtime.
  • compression.threshold0-1 (fraction of the context window that triggers compression; hermes_cli/config.py, default 0.50).

Fields without declared bounds resolve to {}; undefined min/max/step are omitted by React, so every other numeric field renders and behaves exactly as before.

Note on the "slider" wording

The issue calls these sliders; the actual control is a number input. The suggested fix is implemented on that control, plus the typed-value clamp so the bound holds regardless of how the value is entered.

Notes for reviewers

  • This branch was rebuilt from scratch onto current main (19527db731) after the previous head went stale, so it is a fresh commit rather than a rebase of the old ones.
  • Both points from the earlier review are addressed: target_ratio now uses the runtime's 0.10-0.80 rather than 0-1, and the 0 written when the input is cleared is clamped too, with a regression test for a field whose min > 0.
  • Collision warning: PR feat(desktop): localize built-in personality labels in the settings picker #65406 (feat/personality-labels-i18n) edits the same 9-line import block in apps/desktop/src/app/settings/helpers.test.ts, about 2 lines from this PR's edit, which is inside git's 3-line context. Whichever of the two lands second will need a trivial rebase of that import list. No pre-merge was attempted.

Related Issue

Fixes #65703

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 🔒 Security fix
  • 📝 Documentation update
  • ✅ Tests (adding or improving test coverage)
  • ♻️ Refactor (no behavior change)
  • 🎯 New skill (bundled or hub)

Changes Made

  • apps/desktop/src/types/hermes.ts — add optional min / max / step to ConfigFieldSchema so the backend can declare bounds in future.
  • apps/desktop/src/app/settings/constants.ts — new NumberBounds type and NUMBER_BOUNDS map declaring compression.threshold (0-1, step 0.05) and compression.target_ratio (0.10-0.80, step 0.05). Extensible: add a key to bound another numeric field.
  • apps/desktop/src/app/settings/helpers.tsresolveNumberBounds(schemaKey, schema) merges schema-supplied bounds (which win, when finite) over the declared fallback; clampToBounds(n, bounds) clamps into range.
  • apps/desktop/src/app/settings/config-settings.tsx — the number field applies min/max/step to the input and clamps the value in onChange, including the 0 fallback written for a cleared input.
  • apps/desktop/src/app/settings/helpers.test.ts — 11 new cases covering both helpers.

How to Test

  1. Open desktop Settings and go to the Compression section.
  2. Type -0.5 into Target Ratio, then 1.5. Before: both are saved verbatim. After: they clamp to 0.10 and 0.80. compression.threshold clamps to 0 / 1.
  3. Clear the Target Ratio field entirely. Before: 0 is written, which is out of range. After: it clamps to 0.10.
  4. Check any other numeric field (e.g. memory.memory_char_limit): no min/max/step attributes, behavior unchanged.
  5. Automated: cd apps/desktop && npx vitest run --project ui src/app/settings/helpers.test.ts → 41 passed.

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(scope):, feat(scope):, etc.)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature (no unrelated commits)
  • I've run pytest tests/ -q and all tests pass — N/A: this is a TypeScript-only change under apps/desktop, no Python touched. The JS/TS equivalents were run instead, listed below.
  • I've added tests for my changes (required for bug fixes, strongly encouraged for features)
  • I've tested on my platform: macOS 15 (Darwin 25.5, arm64)

Verification actually run (from apps/desktop):

  • npm run typecheck (tsc -p . --noEmit && tsc -p tsconfig.electron.json --noEmit) → clean, exit 0.
  • npx vitest run --project ui src/app/settings/helpers.test.ts41 passed (1 file).
  • npx vitest run --project ui src/app/settings165 passed, 2 failed (18 files).
  • npx vitest run --project ui (full renderer suite) → 1594 passed, 1 skipped, 2 failed (195 files).
  • npx vitest run --project electron458 passed, 1 skipped (45 files).
  • npm run lint0 errors; 9 pre-existing no-restricted-globals warnings, none in the files touched here.
  • npm run fix (eslint --fix + prettier --write) → produced no changes to the diff.
  • git diff --check → clean.

The 2 failures in both --project ui runs are the same two, both in src/app/settings/billing/index.test.tsx (rejects auto-refill amounts outside the billing bounds and disables buy controls while polling and renders the settled outcome). Confirmed pre-existing by stashing this branch's changes and re-running that file against clean main: still 2 failed / 16 passed.

Documentation & Housekeeping

  • I've updated relevant documentation (README, docs/, docstrings) — N/A: no new user-facing behavior beyond enforcing an already-documented range. website/docs/developer-guide/context-compression-and-caching.md already states 0.10-0.80 for target_ratio; this change makes the UI honor it.
  • I've updated cli-config.yaml.example if I added/changed config keys — N/A: no config keys added or renamed, only UI-side bounds on existing keys.
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — N/A: no architecture or workflow change.
  • I've considered cross-platform impact (Windows, macOS) per the compatibility guide — pure renderer-side TypeScript, no platform-specific paths or APIs.
  • I've updated tool descriptions/schemas if I changed tool behavior — N/A: no tool behavior changed.

Screenshots / Logs

Not attached: the change is an input constraint, and its effect is shown more precisely by the unit tests above than by a screenshot of a number field.

@alt-glitch alt-glitch added type/bug Something isn't working comp/desktop Electron desktop app (apps/desktop/*) area/config Config system, migrations, profiles P3 Low — cosmetic, nice to have sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades labels Jul 16, 2026

@tonydwb tonydwb 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.

Code Review Summary

Verdict: Approved

Fix bounds numeric config fields so compression ratios cannot go out of range (+163 -6). Good input validation improvement.


Reviewed by Hermes Agent

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for tracing the generic desktop input path; current main still reproduces the issue at apps/desktop/src/app/settings/config-settings.tsx:145-160.

Problems

  • apps/desktop/src/app/settings/constants.ts:291 gives compression.target_ratio a 0–1 range, but runtime clamps it to 0.10–0.80 in agent/context_compressor.py:1291, and the documented range is also 0.10–0.80 in website/docs/developer-guide/context-compression-and-caching.md:104. The UI should enforce the runtime contract rather than save values that will be normalized later.
  • apps/desktop/src/app/settings/config-settings.tsx:157 writes 0 for an empty field before calling the clamp helper. That bypasses a positive minimum and would remain invalid once target_ratio uses its actual minimum.

Suggested changes

  • Use 0.10–0.80 for compression.target_ratio; retain 0–1 for compression.threshold.
  • Clamp the empty-input fallback too, with a regression test for a field with min > 0.

Automated hermes-sweeper review.

// hermes_cli/config.py (values are fractions, not percentages).
export const NUMBER_BOUNDS: Record<string, NumberBounds> = {
'compression.threshold': { min: 0, max: 1, step: 0.05 },
'compression.target_ratio': { min: 0, max: 1, step: 0.05 }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

target_ratio is not valid across the full 0–1 range: ContextCompressor clamps it to 0.10–0.80 (agent/context_compressor.py:1291), matching the documented range. Please set this fallback to { min: 0.1, max: 0.8, step: 0.05 } so desktop persists the runtime-valid value.

const n = raw === '' ? 0 : Number(raw)

if (raw === '') {
onChange(0)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This bypasses clampToBounds. Once a field has a positive minimum (including target_ratio after correcting its valid range), clearing it saves invalid 0; clamp this fallback or preserve the unset state.

@teknium1 teknium1 added the sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform label Jul 18, 2026
… go out of range

The desktop settings render every `type: 'number'` config field through one
generic `<Input type="number">` with no min/max/step, so compression.threshold
and compression.target_ratio accept negatives and values above their valid
range.

- Add optional min/max/step to `ConfigFieldSchema` so the backend can declare
  bounds; today the desktop supplies them.
- Add a `NUMBER_BOUNDS` map declaring the two compression ratio fields.
  target_ratio uses 0.10-0.80, matching the runtime clamp in
  agent/context_compressor.py and the documented range; threshold uses 0-1.
- `resolveNumberBounds()` merges schema-supplied bounds (which win) with the
  declared fallback; `clampToBounds()` clamps into range.
- The number field applies min/max/step to the input and clamps in onChange,
  including the 0 written when the input is cleared: a bare min/max on an HTML
  number input only constrains the spinner, not typed values.

Fields without declared bounds resolve to `{}`; undefined min/max/step are
omitted by React, so every other numeric field behaves exactly as before.

Fixes NousResearch#65703
@hansai-art
hansai-art force-pushed the fix/desktop-compression-slider-bounds branch from 2485528 to 835c86f Compare July 19, 2026 13:28
@teknium1 teknium1 added the area/compression Context compression and continuation sessions label Jul 19, 2026
@GottZ

GottZ commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

This was generated by AI during triage.

Summary

Eight PRs are associated with this two-issue complex. #65748 addresses #65703 by enforcing runtime-aligned bounds in the generic Desktop number input; #49514, #51020, #61637, #63886, and #67206 cover distinct or overlapping memory-provider work for #49513, while #62459 and #67209 concern TTS/STT provider discovery.

Related pull requests

Duplicates

#49514 substantially overlaps the built-in-memory correction in #67206; #51020 is the source implementation incorporated and extended by #67206; #61637's picker half was superseded by #63886 while its curated-panel half landed separately through #64116; #62459 was incorporated into #67209. None duplicates #65748's #65703 fix.

Suggested consolidation

Keep #65748 open with a salvage path: preserve its generic bounds resolver, runtime-aligned compression ranges, typed-value clamping, and positive-minimum empty-input regression coverage; this follows the contributor keep_open review, whose requested changes are present in the current diff. Treat #49513 as resolved by merged #67206, with closed #49514 and the overlapping #51020/#61637 paths accounted for by the explicit supersession chain; no additional listed PR should be closed as a duplicate of #65748.

Complex graph

flowchart LR
    classDef open fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
    classDef merged fill:#dcfce7,stroke:#15803d,color:#14532d
    classDef closed fill:#e5e7eb,stroke:#6b7280,color:#1f2937
    classDef unverified fill:#f3f4f6,stroke:#9ca3af,color:#374151
    classDef best stroke-width:3px,stroke:#b45309
    classDef target stroke-width:3px,stroke:#4338ca
    I65703(["issue #65703 (open)"])
    P65748["PR #65748 (open)"]
    P65748 -->|best fix| I65703
    class I65703 open
    class P65748 open
    class P65748 best
    class P65748 target
    click I65703 "https://github.com/NousResearch/hermes-agent/issues/65703"
    click P65748 "https://github.com/NousResearch/hermes-agent/pull/65748"
Loading

Graph: solid arrow = fixes / best fix, dashed arrow = partial or unverified (see edge label); boxed group = PRs duplicating each other; amber border = best fix; indigo border = target; gray node = closed (state tag in the node label).

Cross-PR triage: Reviewed 8 pull requests and 2 issues in this complex. Each diff was read against this issue; Assessment working set: 108 kB of PR diffs, 26 kB of issue/PR text, 14 kB of discussion (18 comments), 7 verify verdicts. verdicts reflect diff content, not PR titles. Part of an automated triage batch.

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

Labels

area/compression Context compression and continuation sessions area/config Config system, migrations, profiles comp/desktop Electron desktop app (apps/desktop/*) P3 Low — cosmetic, nice to have sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

UI Bug: Compression settings sliders allow invalid values (negative, >1.0)

5 participants