Skip to main content

Quest Thread and Mission State Machine

Context

This page owns the runtime state-machine shape for Valenar's Quest Thread and Mission primitives. The taxonomy above and below these primitives is owned elsewhere:

What is committed here is the per-instance runtime carrier and the lifecycle states the runtime walks. The binding authority is ../lore/adr/ad-0007-quest-thread-and-mission-scope-primitives.md; this page is the owner-doc surface that ad-0007 routes per-thread and per-mission state-machine concerns to.

The cross-cutting canonical-pattern ADR ../lore/adr/ad-0013-scope-state-channel-template-archetype-pattern.md records that QuestThread and Mission are pattern instantiations: scope X plus state channel plus template<X> archetype. This page follows that pattern verbatim and does not introduce a fifth primitive shape.

Primitive Shape

Quest Threads and Missions are runtime scopes whose authored archetype data lives on matching templates:

  • scope QuestThread carries per-instance runtime state. At minimum: LifecycleState: QuestThreadLifecycleState, ActScopeKind: int, DefinitionTemplateId, ScopedList<Mission> for child missions, and ScopedList<ClueRef> for accumulated clues. The exact field roster beyond this minimum is owned by the Wave 5a runtime-backing wave per ad-0007 lines 47-57.
  • scope Mission carries per-instance runtime state. At minimum: LifecycleState: MissionLifecycleState, DefinitionTemplateId, ParentThreadId, and ScopedList<ActivityBeatRef> for the activity beats the mission contains. Mission walks to its parent QuestThread via the existing SECS scope-walk mechanism.
  • template<QuestThread> and template<Mission> supply identity, narrative metadata, declared act_scope, declared lane key (one of the seven committed cross-act lane keys or act-scoped), authored beat structure, and the binding rules from the dynamic quest pressure contract. The runtime scope rows reference the archetype via DefinitionTemplateId.

No host-side parallel state store may carry QuestThread or Mission per-instance state outside the SECS scope rows. No tag-cluster pattern may stand in for MissionLifecycleState. The "Templates are data" tenet keeps mutable per-instance state on the scope, and the authored archetype on the template.

States

QuestThreadLifecycleState

Seven values, committed by ad-0007 lines 67-70:

StateMeaning
HiddenThread exists at the template layer but the scope row is not yet surfaced.
AvailableDiscovery predicate has fired; the player or policy layer can accept missions under it.
ActiveAt least one child Mission has been accepted; the thread is the player's working surface.
SuspendedPrecondition has been violated mid-run; the thread holds its accumulated clues and waits for the precondition to restore.
ResolvedAll required child Missions resolved per the template's resolution rule.
FailedA critical-path child Mission entered MissionLifecycleState.Failed.
AbandonedPlayer explicitly abandoned the thread (or dismissed it before accepting any Mission).

MissionLifecycleState

Seven values, committed by ad-0007 lines 70-71:

StateMeaning
PendingMission row exists under an Active parent thread but its own preconditions have not fired.
AvailablePreconditions are satisfied; the Mission is offered for acceptance.
AcceptedPlayer or policy committed to running the Mission; queued but not yet executing.
ActiveQueued and the executor is running the Mission's ActivityBeats.
ResolvedAll ActivityBeats resolved per the template<Mission> resolution rule.
FailedAny critical-path ActivityBeat failed.
AbandonedPlayer explicitly abandoned the Mission (or dismissed it before acceptance).

The two enums follow the precedent set by FrontStatus in gd-fronts.md and OperationStatus in gd-operations.md: persistent state expressed as a typed enum on the scope, not as a tag set.

Transitions

The transition rows below are design-level. Each row names a source state, an authoritative trigger, the required facts a runtime predicate must read, the side-effect set, and the target state. Concrete runtime predicate bodies for Suspended / Resolved / Failed / Abandoned on QuestThread, and Resolved / Failed on Mission, are <deferred-to-Wave-5a-runtime-backing> cells: ad-0007 lines 47-57 defers the exact field roster (and therefore the exact predicate shape) to Wave 5a; the design intent below is the binding-doc input this page commits.

QuestThread transitions

SourceTrigger / required factsSide effectsTarget
Hiddentemplate<QuestThread>.DiscoveryTrigger predicate reads true under the dynamic quest pressure generator's evaluation cadence; ActState.CurrentAct >= template.MinAct.on_action QuestThreadAvailable(threadId) (working name — promotion deferred to Wave 5a); Journal entry of class quest-available.Available
AvailablePlayer (or policy layer per ad-0007 Consequences §2) accepts a child Mission whose Pending precondition reads true.on_action QuestThreadActivated(threadId) (working name — promotion deferred to Wave 5a); Current Plan urgency-rank update.Active
ActivePrecondition recorded on template<QuestThread> is violated mid-run. Predicate body <deferred-to-Wave-5a-runtime-backing> per ad-0007 lines 47-57.on_action QuestThreadSuspended(threadId) (working name — promotion deferred to Wave 5a); thread retains ScopedList<ClueRef> and child Mission rows.Suspended
SuspendedPrecondition restored. Predicate body <deferred-to-Wave-5a-runtime-backing> per ad-0007 lines 47-57.on_action QuestThreadResumed(threadId) (working name — promotion deferred to Wave 5a).Active
ActiveAll required child Missions reach MissionLifecycleState.Resolved per the template's resolution rule. Predicate body <deferred-to-Wave-5a-runtime-backing> per ad-0007 lines 47-57.on_action QuestThreadResolved(threadId) (working name — promotion deferred to Wave 5a); Journal entry of class quest-resolved.Resolved
ActiveA critical-path child Mission enters MissionLifecycleState.Failed. Predicate body <deferred-to-Wave-5a-runtime-backing> per ad-0007 lines 47-57.on_action QuestThreadFailed(threadId) (working name — promotion deferred to Wave 5a); Journal entry of class quest-failed.Failed
ActivePlayer commits the Abandon activity against the thread. Activity binding <deferred-to-Wave-5a-runtime-backing> per ad-0007 lines 47-57.on_action QuestThreadAbandoned(threadId) (working name — promotion deferred to Wave 5a); Journal entry of class quest-abandoned.Abandoned
AvailablePlayer dismisses the thread before accepting any Mission. Activity binding <deferred-to-Wave-5a-runtime-backing> per ad-0007 lines 47-57.on_action QuestThreadAbandoned(threadId) (working name — promotion deferred to Wave 5a).Abandoned

Mission transitions

SourceTrigger / required factsSide effectsTarget
PendingParent scope QuestThread reads LifecycleState.Active AND Mission's own template preconditions read true. Predicate body <deferred-to-Wave-5a-runtime-backing> per ad-0007 lines 60-64.on_action MissionAvailable(missionId, threadId) (working name — promotion deferred to Wave 5a); Current Plan candidate-rank update.Available
AvailablePlayer or policy layer commits to the Mission. Activity binding <deferred-to-Wave-5a-runtime-backing> per ad-0007 lines 47-57.on_action MissionAccepted(missionId, threadId) (working name — promotion deferred to Wave 5a).Accepted
AcceptedQueue executor reaches the Mission and starts its first ActivityBeat per the existing queue/activity model in gd-queue-and-activity-execution.md.on_action MissionStarted(missionId) (working name — promotion deferred to Wave 5a); first ActivityBeat begins execution.Active
ActiveAll ActivityBeats resolved per template<Mission>.ResolutionRule. Predicate body <deferred-to-Wave-5a-runtime-backing> per ad-0007 lines 47-57.on_action MissionResolved(missionId, threadId) (working name — promotion deferred to Wave 5a); parent thread ScopedList<Mission> updated; Journal entry of class mission-resolved.Resolved
ActiveAny critical-path ActivityBeat fails. Predicate body <deferred-to-Wave-5a-runtime-backing> per ad-0007 lines 47-57.on_action MissionFailed(missionId, threadId) (working name — promotion deferred to Wave 5a); Journal entry of class mission-failed.Failed
ActivePlayer commits the Abandon activity against the Mission. Activity binding <deferred-to-Wave-5a-runtime-backing> per ad-0007 lines 47-57.on_action MissionAbandoned(missionId, threadId) (working name — promotion deferred to Wave 5a).Abandoned
AvailablePlayer dismisses the Mission before accepting. Activity binding <deferred-to-Wave-5a-runtime-backing> per ad-0007 lines 47-57.on_action MissionAbandoned(missionId, threadId) (working name — promotion deferred to Wave 5a).Abandoned

Triggers and Required Facts

Trigger predicates are named-state predicates only. Day counts, time-since-start counters, and real-time clock values are forbidden as gate inputs anywhere in this state machine — consistent with the binding rule for Act gates in ../lore/adr/ad-0010-act-progression-actstate-scope.md lines 96-99 and the broader "Never tune parameters as a substitute for fixing architecture" tenet.

Inputs available to trigger predicates include:

  • Channels on scope QuestThread and scope Mission themselves.
  • The parent thread's LifecycleState (Mission predicates read this).
  • The template-level DiscoveryTrigger, preconditions, and ResolutionRule declared in template<QuestThread> and template<Mission>.
  • ActState.CurrentAct per ../lore/adr/ad-0010-act-progression-actstate-scope.md.
  • Named-state predicates on existing scopes the dynamic quest pressure generator already reads (Settlement, Territory, Site, Route, MainCharacter, retinue characters).

Mission-candidate generation by the dynamic quest pressure model remains a candidate stream rather than a direct state-machine driver: the generator emits candidates that become scope Mission rows under an Available parent thread, per ad-0007 Consequences §2.

Template Archetypes

The authored content layer uses two template archetypes:

  • template<QuestThread> { ThreadName } carries the thread's identity, declared act_scope, lane key (act-scoped or one of the seven committed cross-act lane keys), DiscoveryTrigger predicate slot, precondition slot, ResolutionRule slot, and authored beat structure. Existing thread owner docs under ../quest-threads/README.md are the design surface; the scope rows that back them are authored in the Wave 5a runtime-backing wave.
  • template<Mission> { MissionName } carries the mission's identity, declared parent-thread reference, ActivityBeat list, precondition slot, and ResolutionRule slot.

Two scopes plus two templates is the canonical pattern instantiation per ad-0013 lines 85-89. No third primitive shape (state-as-tag-set, state-as-host-field-only, state-as-Modifier-flag) is permitted as a fallback.

Side Effects

Every transition fires exactly one on_action label (working name above; the working-name annotation is (working name — promotion to committed metadata deferred to Wave 5a) for each label). The on_action keyword is the committed metadata vocabulary per ../../../../../.claude/rules/behavior-vocabulary.md; the label names themselves are committed only when Wave 5a authors them in Content/quests/events.secs.

Journal entries fire per the existing entry-class taxonomy in ../ux/gd-journal.md. The entry classes used by this state machine are quest-available, quest-resolved, quest-failed, quest-abandoned, mission-resolved, mission-failed. No additional Journal entry class is introduced by this page.

Current Plan urgency-rank updates fire per the existing rank model in ../ux/gd-current-plan.md. The state-machine here does not specify rank values; rank arithmetic is owned by the dynamic quest pressure model.

Mission Resolved on the last required child Mission cascades to the QuestThread Active → Resolved transition when the parent template's ResolutionRule reads satisfied. Mission Failed on a critical-path child cascades to QuestThread Active → Failed the same way. The cascade is the state-machine driver, not a separate event family.

Runtime Backing Status

Status: contract-only.

  • Host/runtime owner: proposed legacy/v1/examples/valenar/Host/Systems/QuestThreadSystem.cs, legacy/v1/examples/valenar/Host/Systems/MissionSystem.cs, legacy/v1/examples/valenar/Host/Data/QuestThreadData.cs, legacy/v1/examples/valenar/Host/Data/MissionData.cs. None exist today. Authored in the Wave 5a runtime-backing wave per ad-0007 lines 137-145.
  • Generated/.secs owner: legacy/v1/examples/valenar/Content/quests/scopes.secs (new scope QuestThread, scope Mission), Content/quests/templates.secs (new template<QuestThread>, template<Mission>), Content/quests/events.secs (the working-name on_action labels promoted to committed labels), and any future Content/quests/channels.secs for derived per-thread channels. Matched by the parallel legacy/v1/examples/valenar/Generated/Quests/ stand-in tree.
  • Read-model/UI owner: proposed legacy/v1/examples/valenar/Host/ReadModels/QuestThreadsReadModel.cs and legacy/v1/examples/valenar/Host/ReadModels/MissionsReadModel.cs. Neither exists today. UI consumers are ../ux/gd-current-plan.md, ../ux/gd-journal.md, and ../ux/gd-objectives-screen.md.
  • Tests: <deferred-to-wave-7> — Wave 7 test-authoring wave authors unit tests against each transition row. Test names use the <StateMachine><Source>To<Target>Test convention (e.g. QuestThreadHiddenToAvailableTest).
  • Known gaps: the exact field roster on scope QuestThread and scope Mission beyond the minimum committed in ad-0007; the ActScopeKind enum's exact values; the DiscoveryTrigger, precondition, and ResolutionRule slot signatures on the two template archetypes; the working-name on_action labels' promotion to committed labels.
  • Illegal fallback behavior: no silent mission generation outside the dynamic quest pressure generator; no fake urgency strings outside the Current Plan rank model; no host-side parallel state store; no tag-cluster pattern substitute for MissionLifecycleState. An attempt to set LifecycleState directly on a scope row outside the declared transitions raises an explicit InvalidStateException rather than silently mutating state.
  • Next closure wave: Wave 5a runtime-backing wave authors the host data classes, the Content/quests/*.secs source, the matching Generated stand-ins, and the read-model surfaces. The Journal-readback hook wave follows.

Glossary Propagation

The following terms must land in the canonical glossary surfaces (../gd-glossary.md, ../gd-canon.md, ../README.md) per ../../../../../.claude/rules/valenar-contract-backing.md § Canonical Glossary Targets. Glossary propagation itself is the next docs wave (Wave 3c); this section records the obligation:

  • QuestThread (scope primitive).
  • Mission (scope primitive).
  • QuestThreadLifecycleState (enum; seven values listed above).
  • MissionLifecycleState (enum; seven values listed above).
  • ActScopeKind (enum; values deferred to Wave 5a).
  • DefinitionTemplateId (template id reference field on the two scopes).
  • ScopedList<Mission> (child-mission list field on scope QuestThread).
  • ScopedList<ClueRef> (accumulated-clue list field on scope QuestThread).
  • ScopedList<ActivityBeatRef> (activity-beat list field on scope Mission).

Cross-References