Skip to content

FableLoom sync: preserveLegacyVisualProduction strips delivery plans, outlines, and audience mode when merging from older peers #6005

Description

@atomantic

Problem

In server/services/fableLoom/records.js:641-718, preserveLegacyVisualProduction is designed to retain newer fields when an older federated peer (sender schema version < 6) sends a whole-record LWW update for a story. However, several additive fields added in schema versions 4 and 5 are not preserved when merging from an older peer:

  1. Series delivery plan (senderVersion < 5, records.js:663-669):

    seriesPlan: senderVersion < 5 ? {
      ...remote.seriesPlan,
      plotPoints: (remote.seriesPlan?.plotPoints || []).map((item) => ({
        ...item,
        ...(localPlotPoints.has(item.id) ? { kind: localPlotPoints.get(item.id).kind } : {}),
      })),
    } : remote.seriesPlan,

    It spreads ...remote.seriesPlan and only preserves plotPoints[].kind. It does not preserve local.seriesPlan.deliveryOptions, local.seriesPlan.interEpisodeVoicemails, or local.seriesPlan.nextSeasonTeaser. Older peers lack delivery options, so sanitizeSeriesPlan (records.js:209-211) evaluates hasDeliveryPlan = false and omits them completely from remote.seriesPlan. The merge writes this back, permanently deleting the local story's delivery options and inter-episode voicemails.

  2. Episode beat outlines (senderVersion < 5, records.js:682-693):

    ...(senderVersion < 5 && episode.storyOutline ? {
      storyOutline: {
        ...episode.storyOutline,
        scenes: episode.storyOutline.scenes.map((scene) => ({
          ...scene,
          ...(localOutlineScenes.has(scene.key) ? {
            plotPointId: localOutlineScenes.get(scene.key).plotPointId,
            challengePhase: localOutlineScenes.get(scene.key).challengePhase,
          } : {}),
        })),
      },
    } : {}),

    The check requires episode.storyOutline to exist on the remote record. When an older peer (or an install that authored the episode before outlines existed) sends an episode without storyOutline, episode.storyOutline is undefined/falsy. The check evaluates to {} and completely ignores localEpisode.storyOutline. As a result, the local episode's beat outline and validation state are wiped out.

  3. Audience participation mode and medium (senderVersion < 4, records.js:670-674):
    Lines 670-674 only preserve protagonist bindings:

    ...(senderVersion < 4 ? {
      protagonistCharacterId: local.protagonistCharacterId,
      protagonistWardrobeId: local.protagonistWardrobeId,
      protagonistWardrobeLocked: local.protagonistWardrobeLocked,
    } : {}),

    A <=v3 sender does not know participationMode or audienceCommunicationMedium. On arrival, sanitizeLoom defaults participationMode to 'protagonist' and audienceCommunicationMedium to ''. Because preserveLegacyVisualProduction does not preserve them, an update from an older peer silently downgrades a local 'helper' story with a configured communication medium back to 'protagonist' with an empty medium.

Trigger

  1. An upgraded PortOS install (holding a FableLoom story with delivery options, inter-episode voicemails, episode beat outlines, and helper participation mode) is paired with a peer running an older PortOS version (fableLoom wire schema version 1, 2, 3, or 4).
  2. The older peer makes an edit (e.g. renaming the story or editing scene prose) and pushes the record (POST /api/peer-sync/push handled by server/services/sharing/peerSyncReceive.js:342).
  3. The receiver accepts the push because the sender is behind rather than ahead (versionDiff.ahead is empty in peerSyncReceive.js:211).
  4. mergeLoomsFromSync invokes preserveLegacyVisualProduction(remote, local, senderVersion) and persists the merged record via writeRaw.
  5. The local story's delivery options, voicemails, teasers, beat outlines, and participation mode are silently erased or reverted.

Impact

Data loss across peer sync in mixed-version federations. Upgraded PortOS installs lose author-configured delivery options, inter-episode voicemail transcripts, season teasers, episode beat outlines, and audience participation settings whenever a federated peer on an older version syncs an edit.

Fix

In server/services/fableLoom/records.js:

  • In preserveLegacyVisualProduction:
    • When senderVersion < 5: preserve deliveryOptions, interEpisodeVoicemails, and nextSeasonTeaser from local.seriesPlan if present and omitted by remote.
    • When mapping episodes under senderVersion < 5: if episode.storyOutline exists on remote, merge scene keys with localOutlineScenes as today; if episode.storyOutline is absent on remote but localEpisode?.storyOutline exists, preserve localEpisode.storyOutline.
    • When senderVersion < 4: preserve local.participationMode and local.audienceCommunicationMedium.
  • In server/services/fableLoom/records.test.js:
    • Add test cases verifying that mergeLoomsFromSync with a v4 peer preserves local deliveryOptions, interEpisodeVoicemails, and nextSeasonTeaser.
    • Add test case verifying that mergeLoomsFromSync with a v4 peer lacking episode outlines preserves local storyOutline.
    • Add test case verifying that mergeLoomsFromSync with a v3 peer preserves local participationMode and audienceCommunicationMedium.

Alternative rejected: Bumping PORTOS_SCHEMA_VERSIONS.fableLoom to 7 to 409-reject pushes from older peers. Rejected because PortOS explicitly uses preserveLegacyVisualProduction and graceful degradation so peers on independent update schedules can continue syncing without breaking the federation.

Acceptance Criteria

  • preserveLegacyVisualProduction retains local.seriesPlan.deliveryOptions, local.seriesPlan.interEpisodeVoicemails, and local.seriesPlan.nextSeasonTeaser when senderVersion < 5.
  • preserveLegacyVisualProduction retains localEpisode.storyOutline when senderVersion < 5 and the remote episode has no outline.
  • preserveLegacyVisualProduction retains local.participationMode and local.audienceCommunicationMedium when senderVersion < 4.
  • Vitest suite server/services/fableLoom/records.test.js passes with test coverage for v4 delivery plan, v4 outline, and v3 participation mode preservation.

Activity

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

Metadata

Metadata

Assignees

Labels

area:federationCross-machine sync & federationarea:pipelineComic/story production pipelinebugSomething isn't workingeffort:mediumEffort: mediummodel:mediumModel size: mediumplanTracked by /do:replanplanner:gemini-3-7-flashPlan authored by the gemini-3-7-flash model

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions