nesquena/hermes-webui

Stale tab can overwrite the WebUI update channel during an unrelated Preferences autosave

已关闭

#6,612 创建于 2026年7月29日

 (2 条评论) (0 个反应) (0 位负责人)Python (2,386 个派生)github user discovery
bughelp wantedsprint-candidate

仓库指标

星标
 (17,368 个星标)
PR 合并指标
 (平均合并 14小时 31分钟) (30 天内合并 314 个 PR)

描述

Summary

A stale WebUI tab can overwrite a newer update-channel selection when the user changes any unrelated preference. Preferences autosave serializes the tab's entire visible Preferences state, including update_channel, on every control change. A tab that still holds experimental can therefore silently revert a later stable choice made in another tab.

This is an agent-reproduced candidate uncovered while investigating a Discord report of an unexpected Stable → Experimental switch. The historical report proves that the installation applied the Experimental release, but it does not prove that this stale-tab path was the initiating event.

Related report: https://discord.com/channels/1492983281338286121/1493325333980774622/1527180634097741884

Deterministic sequence

  1. Select Experimental and leave Tab A open. Its settingsUpdateChannel control now holds experimental.

  2. In Tab B, select Stable and let that save complete.

  3. Without refreshing Tab A, toggle any unrelated preference there, such as TPS display or sidebar density.

  4. Tab A's generic Preferences autosave sends its full stale payload, including:

    {
      "update_channel": "experimental",
      "<unrelated_preference>": "<new value>"
    }
    
  5. save_settings() accepts the explicit allowed enum value, so the server silently switches back to Experimental. A subsequent update check/apply legitimately targets the Experimental tag stream.

Current source

On current origin/master (0a4015975):

  • static/panels.js::_preferencesPayloadFromUi() always includes the selector value:

    const updateChannelSel=$('settingsUpdateChannel');
    if(updateChannelSel) payload.update_channel=updateChannelSel.value;
    
  • _schedulePreferencesAutosave() builds that full payload for every preference change and posts it to /api/settings.

  • api/config.py::save_settings() correctly treats either stable or experimental as an explicit valid write; it has no client revision or per-field stale-write guard.

  • Update checks and applies only consume the persisted channel. They do not independently invent or migrate stable to experimental.

Historical report reconciliation

The reported installed descriptor v0.52.76-214-g478f7749 maps exactly to commit 478f7749, tagged exp-v0.52.140. Recreating the 2026-07-22 tag horizon showed:

  • Stable on v0.52.76: up to date
  • Experimental from that checkout: 64 channel releases behind, latest exp-v0.52.140
  • Applying Experimental yields the reporter's exact stable-family descriptor

So the channel was genuinely Experimental at apply time. What remains unknown is which client action wrote that setting.

Expected behavior

Changing an unrelated preference in a stale tab must not rewrite the update channel. Update-channel changes should be persisted only from an explicit interaction with that control, or protected by a settings revision/conflict mechanism.

Suggested fix direction

Narrow fix:

  • Remove update_channel from the generic Preferences autosave payload.
  • Give the channel selector a dedicated write path that sends update_channel only when the user changes that selector.
  • Re-sync the selector from the server response after a successful write.

Broader alternative: add revision-based optimistic concurrency for settings autosaves. That addresses the same last-write-wins class across all full-form preferences but is materially larger.

Acceptance criteria

  • Tab A may hold a stale channel value while Tab B changes the persisted channel.
  • Changing an unrelated preference in Tab A does not alter the persisted channel.
  • An explicit channel change still persists and immediately drives channel-scoped check/apply state.
  • A regression test exercises the two-client stale-value sequence.
  • Unknown/invalid/missing channel values continue to normalize to Stable.

贡献者指南