mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-20 15:33:54 +00:00
fix(desktop): prevent MoA autosave defaults explosion from half-filled slots
Three fixes in model-settings.tsx: 1. Filter incomplete slots before autosave: sanitizeMoaRefsForSave() strips reference slots with empty model before any autosave (the 600ms debounce was sending half-filled provider-but-no-model slots to the backend, where _clean_slot rejects them and _normalize_preset falls back to hardcoded defaults). Presets with zero valid refs keep their empty reference_models array rather than being silently dropped. The aggregator slot is also sanitized when its model is empty. 2. Add withActive() to MoA provider dropdowns: the reference and aggregator provider Selects filtered to authenticated-only, so unauthenticated current values (e.g. openai-codex) rendered blank. Mirror the existing pattern from the model Select. 3. Add generation counter to scheduleMoaSave(): stale save responses could overwrite newer state. Bump a counter on each save and skip setMoa/setError if a newer save was scheduled in the meantime.
This commit is contained in:
parent
f8630a1456
commit
a61a0bc019
1 changed files with 63 additions and 14 deletions
|
|
@ -317,6 +317,38 @@ export function ModelSettings({ onMainModelChanged }: ModelSettingsProps) {
|
|||
[]
|
||||
)
|
||||
|
||||
// Guard against stale save responses overwriting newer state.
|
||||
const moaSaveGeneration = useRef(0)
|
||||
|
||||
// Strip slots with an empty model from every preset before autosave, so a
|
||||
// half-filled slot (provider selected but no model yet) is never sent to the
|
||||
// backend. Without this guard, _clean_slot rejects the empty-model slot and
|
||||
// _normalize_preset falls back to hardcoded defaults. Both reference and
|
||||
// aggregator slots are sanitized. Presets keep their empty reference_models
|
||||
// array rather than being dropped.
|
||||
const sanitizeMoaRefsForSave = useCallback((config: MoaConfigResponse): MoaConfigResponse => {
|
||||
const presets: MoaConfigResponse['presets'] = {}
|
||||
let changed = false
|
||||
|
||||
for (const [name, preset] of Object.entries(config.presets)) {
|
||||
const refs = preset.reference_models.filter(slot => slot.provider.trim() && slot.model.trim())
|
||||
if (refs.length !== preset.reference_models.length) {
|
||||
changed = true
|
||||
}
|
||||
const agg = preset.aggregator
|
||||
const aggValid = agg && agg.provider.trim() && agg.model.trim()
|
||||
const cleanAgg = aggValid ? agg : { provider: agg?.provider ?? '', model: agg?.model ?? '' }
|
||||
|
||||
presets[name] = {
|
||||
...preset,
|
||||
reference_models: refs,
|
||||
aggregator: cleanAgg
|
||||
}
|
||||
}
|
||||
|
||||
return changed ? { ...config, presets } : config
|
||||
}, [])
|
||||
|
||||
// Quiet debounced persist for inline MoA edits — mirrors the config page's
|
||||
// autosave so slot/aggregator tweaks save themselves, matching the
|
||||
// preset-level ops (set default / add / delete) that already persist on
|
||||
|
|
@ -326,12 +358,23 @@ export function ModelSettings({ onMainModelChanged }: ModelSettingsProps) {
|
|||
window.clearTimeout(moaSaveTimer.current)
|
||||
}
|
||||
|
||||
const generation = moaSaveGeneration.current + 1
|
||||
moaSaveGeneration.current = generation
|
||||
|
||||
moaSaveTimer.current = window.setTimeout(() => {
|
||||
void saveMoaModels(next)
|
||||
.then(setMoa)
|
||||
.catch(err => setError(err instanceof Error ? err.message : String(err)))
|
||||
void saveMoaModels(sanitizeMoaRefsForSave(next))
|
||||
.then(saved => {
|
||||
if (moaSaveGeneration.current === generation) {
|
||||
setMoa(saved)
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
if (moaSaveGeneration.current === generation) {
|
||||
setError(err instanceof Error ? err.message : String(err))
|
||||
}
|
||||
})
|
||||
}, 600)
|
||||
}, [])
|
||||
}, [sanitizeMoaRefsForSave])
|
||||
|
||||
const updateMoaPreset = useCallback(
|
||||
(updater: (preset: NonNullable<typeof currentMoaPreset>) => NonNullable<typeof currentMoaPreset>) => {
|
||||
|
|
@ -991,11 +1034,14 @@ export function ModelSettings({ onMainModelChanged }: ModelSettingsProps) {
|
|||
<SelectValue placeholder={m.provider} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{moaSlotProviderOptions.map(provider => (
|
||||
<SelectItem key={provider.slug || 'none'} value={provider.slug || 'none'}>
|
||||
{provider.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
{withActive(moaSlotProviderOptions.map(p => p.slug || 'none'), slot.provider).map(slug => {
|
||||
const provider = moaSlotProviderOptions.find(p => (p.slug || 'none') === slug)
|
||||
return (
|
||||
<SelectItem key={slug} value={slug}>
|
||||
{provider?.name || slug}
|
||||
</SelectItem>
|
||||
)
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select
|
||||
|
|
@ -1070,11 +1116,14 @@ export function ModelSettings({ onMainModelChanged }: ModelSettingsProps) {
|
|||
<SelectValue placeholder={m.provider} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{moaSlotProviderOptions.map(provider => (
|
||||
<SelectItem key={provider.slug || 'none'} value={provider.slug || 'none'}>
|
||||
{provider.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
{withActive(moaSlotProviderOptions.map(p => p.slug || 'none'), currentMoaPreset.aggregator.provider).map(slug => {
|
||||
const provider = moaSlotProviderOptions.find(p => (p.slug || 'none') === slug)
|
||||
return (
|
||||
<SelectItem key={slug} value={slug}>
|
||||
{provider?.name || slug}
|
||||
</SelectItem>
|
||||
)
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue