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.

Activity

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

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