Skip to content

[Bug]: Config written by a newer version breaks startup on downgrade (missing field 'flowChat') #2717

Description

@1688mengdie

Summary

When a newer BitFun version that has retired the font.flowChat preference writes to the shared per-user configuration directory, the resulting config/app.json no longer contains the flowChat key. An older 0.2.x release that still treats flowChat as a required field (it carries no #[serde(default)]) then fails to deserialize the config on startup with missing field 'flowChat'. The built-in smart merge does not recover, because the field lives inside an Option<FontPreferenceSnapshot> whose default base is None, so there is no font key in the merge base to repair against.

Expected: older releases should tolerate configs written by newer releases (forward compatibility, e.g. #[serde(default)] retained across a few releases before removal), or different versions should not silently share one configuration directory.

Area

Desktop app

Reproduction or evidence

Steps to reproduce

  1. Use a newer BitFun build that has dropped the retired font.flowChat preference; it writes its config to the per-user configuration directory that is shared across versions.
  2. Close that build and launch an older 0.2.x release that still declares flow_chat as a required field of FontPreferenceSnapshot.
  3. Observe the startup config-recovery prompt. Starting a conversation then reports that the primary model cannot be resolved.

Actual behavior

The older release fails to deserialize the config and enters a "smart merge" recovery path. The config-manager log reports:

[WARN][bitfun_core::service::config::manager]
Config file deserialization failed, starting smart merge: missing field `flowChat`

The recovery writes the config back essentially unchanged: the "restored" file is byte-for-byte identical (same length) to the quarantined copy, so the failure repeats on every launch and is never truly resolved. The recovery also replaces the in-memory config with factory defaults, which is why starting a dialog turn then fails to resolve a concrete primary model.

Expected behavior

Older releases should be able to deserialize configs written by newer releases. A required field that is retired in a newer version should either remain forward-compatible (e.g. #[serde(default)] retained for some releases before removal), or be handled structurally so the config stays readable.

Root cause analysis (from a maintainer perspective)

  1. Retired-field cleanup without forward compatibility. The released config types declare flow_chat as a required member of FontPreferenceSnapshot with no #[serde(default)]:

    struct FontPreferenceSnapshot {
        ui_size:   UiSizeSnapshot,          // has a default
        flow_chat: FlowChatFontSnapshot,    // required (no #[serde(default)])
    }
    

    The newer direction removes flow_chat from FontPreferenceSnapshot and adds a test that ignores retired flowChat data without re-emitting it. That removal path is correct for a single version, but the field was never given a #[serde(default)] bridge, so a config written by the newer version cannot be read by the older version.

  2. Smart merge is a blind spot for required sub-fields nested inside an Option<T>. The font member is declared:

    pub font: Option<FontPreferenceSnapshot>   // default base is None
    

    The deep-merge routine recurses over the default config, but the default has no font key at all, so when the user config carries a font object whose internal required field is missing, the merge adopts the user value as-is and carries the defect through. This is a general trap: a required struct field nested inside an Option<T> cannot be repaired by the current smart merge.

  3. Versions share one config directory without isolation. Different versions write to the same per-user configuration directory, so a downgrade silently loads a config produced by a newer schema.

Evidence confirming the field is required and that the change is version-driven

  • In the current mainline config types (src/crates/assembly/core/src/service/config/types.rs), FontPreferenceSnapshot still declares flow_chat: FlowChatFontSnapshot, confirming flow_chat is required in a released version.
  • The newer direction (referenced by the 1.0.0-explore branch) removes flow_chat from FontPreferenceSnapshot and adds a test that ignores retired flowChat data without re-emitting it.

Requested solution (constructive request)

One or both of the following would help:

  • (a) Cross-version migration compatibility. Retain #[serde(default)] on retired fields for a few releases before removing them, so configs written by newer versions remain readable by older ones; and/or extend smart merge to handle required sub-fields inside Option<T> by adding structural validation plus default-value backfill.
  • (b) Isolate the configuration directory by major version. Use a separate config directory per major version (or provide a one-click separation), so a downgrade never loads a config produced by a newer schema.

Environment, if relevant

  • BitFun version: the failing release is a 0.2.x desktop build (reported as 0.2.19).
  • A newer build aligned with the 1.0.0-explore direction wrote the config before the downgrade.
  • OS: Windows (per-user roaming application data directory).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions