Saving a relationship is only useful if the next conversation reads the restored value. While tracing the public Interactive NPC for Unity source, I found a lifecycle path where the controller and its prompt builder can end up holding different relationship trackers.
This is a source-level finding from the reviewed commit, not a bug I reproduced by running the Unity scene. It occurs when a saved relationship exists and the Start load path runs after the prompt builder has been created.
- 01 / AWAKEWire the prompt builder
controller → R₀builder._relationship → R₀Both fields point to the same new tracker. - 02 / STARTRestore saved state
controller → R₁builder._relationship → R₀The controller replaces its field; the builder keeps its captured reference. - 03 / MESSAGE + SAVETwo consumers, two statesThe mismatch appears only when a saved relationship is loaded.
PROMPT BUILDER
reads and updates R₀CONTROLLER
exposes and saves R₁
The reference changes after the builder is wired
In Awake, InitializeSystems() creates a new RelationshipTracker and passes it to PromptBuilder. In Start, LoadPersistedData() reads the saved relationship and assigns a different tracker to the controller field. The controller initialization and restore path makes that order visible.
PromptBuilder stores its constructor argument in a readonly field, so replacing the controller’s field does not update the builder’s reference. Its request path later applies sentiment changes to that captured tracker and reads relationship context from it when composing the system prompt (sentiment handling, prompt context).
The split is conditional: if there is no saved relationship to restore, both references still point to the original tracker. When saved data is present, the first greeting can use the restored controller tracker, while the next generated request can describe and mutate the old one. At save time, the controller serializes its current tracker, so sentiment-driven changes made through the builder may not be the changes that get written back.
Why component tests can miss a lifecycle defect
The repository’s editor test file covers focused behavior in the memory bank, relationship tracker, mood controller, personality traits, and response modifier. Those checks are useful, but the checked-in suite does not exercise the whole controller sequence: create dependencies, restore a save, build a prompt, update a relationship, then save again.
That distinction matters in Unity code. Each object can pass its own tests while a consumer still points at an earlier instance. The invariant here is not only “the saved score loads”; it is “the controller, prompt builder, and persistence path all observe the same live relationship state.”
Restore state before wiring consumers
I would keep one tracker instance for the NPC and restore the saved values into it before the prompt builder is used. Another safe option is to finish loading first and construct the builder with the restored tracker. If a tracker must be replaced, every object that captured it needs to be rebuilt or updated at the same boundary.
The regression test should use a known saved relationship, then inspect the assembled LLMRequest.SystemPrompt and the tracker that the controller will save. It should also send a deterministic compliment or insult through the prompt builder and verify that the same live tracker changes. The assertion should target the context and state handoff, not the wording returned by a language model.
The source review identifies a concrete risk and a testable contract; it does not establish how often a player would notice it in a running build. I would confirm the fix in the Unity Test Runner with a save-and-reload lifecycle test before describing the behavior as runtime-verified.
For a related example of testing the boundary between a request and state, see how FieldCore handles an exact sync retry and how Retentia AI moves synthetic examples through its model pipeline.