What is the current behavior?
Starting a new dialogue from a continuation of DialogueRunner.DialogueTask corrupts the runner's state. The new dialogue presents its first line and then stops: the line dismisses normally (blank text field from ContentDidDismiss, continue button disabled by OnLineWillDismiss), but no further content is ever requested. Nothing is logged, no exception is thrown, and IsDialogueRunning stays true — the VM is alive and simply never told to continue. The dialogue UI stays up and blocks input, so it presents to the player as a hard freeze.
The new dialogue's DialogueTask is also lost, so anything awaiting it hangs regardless.
Please provide the steps to reproduce, and if possible a minimal demo of the problem:
Two nodes of two or more lines each, started back to back with nothing that yields in between:
await runner.StartDialogue("NodeA");
await runner.DialogueTask;
await runner.StartDialogue("NodeB"); // runs re-entrantly inside NodeA's teardown
await runner.DialogueTask; // never completes
NodeA plays to the end, NodeB shows its first line; advancing that line dismisses it and the dialogue never resumes. Inserting any real await between the two calls (await YarnTask.Yield(), a scene transition, a popup) makes it disappear — so in a real project the same pair of nodes works or breaks depending on what happened to yield in between.
What is the expected behavior?
StartDialogue called from a DialogueTask continuation starts a normal, fully functional dialogue. The runner should not overwrite state belonging to a dialogue that has already started.
Please tell us about your environment
- Operating System: Windows 11 (24H2)
- Yarn Spinner Version: 3.2.7 (Unity package, git #v3.2.7)
- Extension Version: n/a (not using the VS Code extension)
- Unity Version: 6000.3.18f1
Other information
Ordering problem in DialogueRunner.OnDialogueCompleteAsync (line numbers are v3.2.7). The completion source is signalled before the runner tears down its state, and continuations resume synchronously, so the awaiting code runs in the middle of this method (~line 634):
dialogueCompletionSource?.TrySetResult(); // awaiting code resumes here and calls
// StartDialogue("NodeB")
onDialogueComplete?.Invoke();
dialogueCancellationSource?.Dispose(); // disposes NodeB's cancellation source
dialogueCancellationSource = null;
dialogueCompletionSource = null; // drops NodeB's completion source
This is deterministic, not a race: StartDialogue assigns dialogueCompletionSource and dialogueCancellationSource at lines 1069–1073, before its first await, so the fields are always freshly populated by the time control returns to the Dispose()/null lines — no matter how the presenters' OnDialogueStartedAsync behave.
Two consequences for the dialogue now running:
1. OnLineReceivedAsync (~line 766) ends with if (dialogueCancellationSource?.IsCancellationRequested == false) { Dialogue.Continue(); }. With the field null the comparison is false, so Dialogue.Continue() is never called and delivery stops after the current line. A null field is indistinguishable here from a cancelled one, which is why this fails without any diagnostic.
2. dialogueCompletionSource is null, so dialogueCompletionSource?.TrySetResult() at the end of the next OnDialogueCompleteAsync is a no-op and DialogueTask never resolves.
Suggested fix — tear down before signalling, so re-entrant StartDialogue is safe:
var completion = dialogueCompletionSource;
var cancellationCompletion = dialogueCancellationCompletion;
dialogueCancellationSource?.Dispose();
dialogueCancellationSource = null;
dialogueCompletionSource = null;
completion?.TrySetResult();
onDialogueComplete?.Invoke();
cancellationCompletion?.TrySetResult();
Worth tightening dialogueCancellationSource?.IsCancellationRequested == false to != true (or an explicit null check that warns) either way: any future path that nulls the field mid-dialogue will silently stop content delivery rather than fail loudly.
User-side workaround — unwind the stack before caller code runs:
await runner.StartDialogue(node);
await runner.DialogueTask;
await UniTask.Yield(PlayerLoopTiming.LastPostLateUpdate); // same frame, outside the teardown
Add tags
Unity
What is the current behavior?
Starting a new dialogue from a continuation of
DialogueRunner.DialogueTaskcorrupts the runner's state. The new dialogue presents its first line and then stops: the line dismisses normally (blank text field fromContentDidDismiss, continue button disabled byOnLineWillDismiss), but no further content is ever requested. Nothing is logged, no exception is thrown, andIsDialogueRunningstaystrue— the VM is alive and simply never told to continue. The dialogue UI stays up and blocks input, so it presents to the player as a hard freeze.The new dialogue's
DialogueTaskis also lost, so anything awaiting it hangs regardless.Please provide the steps to reproduce, and if possible a minimal demo of the problem:
Two nodes of two or more lines each, started back to back with nothing that yields in between: