Skip to content

A session belongs to exactly one campaign, and nothing decides what happens if content spans two #293

Description

@The-Running-Dev

A session belongs to exactly one campaign, and nothing decides what happens if content spans two

A proposal to let a scene offer a choice leading into another campaign's content — a side quest bridging Campaign A into Campaign B — has no answer today for what the session becomes at that moment, and the failure mode is silent rather than loud.

GameState.campaignId is singular. resolveSaveEnvelope cross-checks that the envelope's campaignId matches the embedded state's own and rejects a mismatch as invalid_state. Achievements are recorded per (campaign, achievement). Downstream hosts denormalize campaign_id per session and group player progress by it. Every one of those assumes a session sits in one campaign for its whole life.

The sharp part is not that a bridge would fail these checks. It is that it might not fail at all. Story-graph variables are per-campaign namespaces, but the committed campaigns do not use distinct names — every campaign built through adventure-builder.ts declares the same route, preparation, connections and pressure. Carrying Campaign A's variable state into Campaign B's nodes would not error; it would silently reinterpret A's values as B's, and the run would continue looking entirely normal while meaning something different. A crash would be better.

So the semantics have to be decided before any bridging content exists, not discovered by building one. The candidate shapes each imply different state:

  • Jump — the session becomes Campaign B. Then what happens to A's variables, A's visitedCounts, and A's unfinished ending set?
  • Call and return — the session is in B temporarily and comes back. That implies a stack in StoryGraphKindState, which does not have one.
  • Nested session — B is a separate session linked to A. Cleanest for state isolation, but the player-facing "one continuous run" illusion then has to be assembled by the host.
  • Not supported — bridging is expressed as content within one campaign, and cross-campaign reachability is a catalog concern rather than a traversal one.

This issue is the decision, not the implementation.

Done when

  • A decision is recorded in design/90-decisions.md for what a session is when content crosses a campaign boundary — including the "not supported" outcome, if that is the answer
  • If any form of crossing is supported, the fate of variables, visitedCounts, achievements and ending sets at the boundary is specified
  • If crossing is supported, the shared variable-name collision across adventure-builder.ts campaigns is addressed rather than left as a latent silent-reinterpretation bug

Agent instructions

Origin: evaluation of an external design handoff proposing cross-campaign side quests. That document explicitly left these semantics "deliberately not decided" — but its own first experiment would hit them immediately, which is why this is filed as a decision rather than folded into a spike.

Read first: src/engine/src/core/kernel/types.ts (GameState), src/engine/src/core/persistence/envelope.ts (the campaignId cross-check), src/engine/src/kinds/story-graph/state.ts, and src/engine/src/campaigns/adventure-builder.ts (the shared variable declarations).

Do not implement a bridge, a session stack, or a campaign-linking mechanism as part of answering this. The output is a recorded decision.

Stop if: the chosen answer requires a change to GameState's shape or to the save envelope's validation rules — that is a contract change and goes through design/ first.

Metadata

Metadata

Assignees

No one assigned

    Labels

    questionFurther information is requested

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions