Problem
The eca_event task snapshots the ECA token bag before dispatching its custom event and restores it afterwards, so that one request advancing several eca_event nodes cannot let one node's values be read by the next. The restore never restores anything.
ECA keeps a scalar under a name by wrapping it in a DataTransferObject, and on every later write to that name it reuses the object already stored rather than making a new one:
if ($this->hasTokenData($key)) { $current_data = $this->getTokenData($key); $dto = $current_data instanceof DataTransferObject ? $current_data : DataTransferObject::create($current_data); } ... if (is_scalar($data)) { $dto->setStringRepresentation($data); }
So getTokenData() answers the container, not the value in it. The task saves that container, then writes its own value into the very same container, and restoring it writes the container back over itself. The pre-dispatch value is gone the moment the task exposes its own, and there is no second copy anywhere to restore from.
Within one Orchestra cascade this is invisible: ECA stores an empty string as NULL, so the preceding node's restore-to-empty really does unset the names and the next node finds them clear. What is lost is a name that already held a value when the node ran. That is the value a caller holds under the node's configured outcome token, and, when a reacting model synchronously drives another eca_event node, the outer node's own token and instance IDs.
The same shape was fixed in the timeout subscriber in #3620621, where a test seeding a value, dispatching, and reading it back proved the restore was a no-op.
Proposed resolution
Read the value and clone it when it is a DataTransferObject, so the snapshot is a copy rather than the container the dispatch goes on to write into:
$value = $this->tokenServices->getTokenData($name); $snapshot[$name] = $value instanceof DataTransferObject ? clone $value : $value;
Map::__clone() deep-clones the properties, so a later write to the name cannot reach the snapshot, and an entity or a structured value is given back as itself rather than flattened to an ID or a Yaml blob. An absent name snapshots as NULL, which addTokenData() already stores as no value at all, so the restore needs no coercion through the empty string.
Two kernel tests cover it. The first seeds the node's configured outcome token, answers the event with something else, and asserts the node both recorded the answer and gave the seeded value back. The second is the nested case the snapshot exists for: the outer node's model answers and then starts a second synchronous workflow inline, so the inner eca_event node writes the same bag names inside the outer node's dispatch, and the outer node has to route on its own answer and still see its own instance ID.
This issue summary was drafted with the assistance of an AI agent (Claude). The analysis and the wording were reviewed by me before posting, and accountability for the content is mine.
Issue fork orchestra-3621217
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #3
mably commentedComment #5
mably commentedComment #7
mably commentedComment #8
mably commented