Stale tab can overwrite the WebUI update channel during an unrelated Preferences autosave
#6,612 创建于 2026年7月29日
仓库指标
- 星标
- (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
-
Select Experimental and leave Tab A open. Its
settingsUpdateChannelcontrol now holdsexperimental. -
In Tab B, select Stable and let that save complete.
-
Without refreshing Tab A, toggle any unrelated preference there, such as TPS display or sidebar density.
-
Tab A's generic Preferences autosave sends its full stale payload, including:
{ "update_channel": "experimental", "<unrelated_preference>": "<new value>" } -
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 eitherstableorexperimentalas 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
stabletoexperimental.
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_channelfrom the generic Preferences autosave payload. - Give the channel selector a dedicated write path that sends
update_channelonly 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.