fix: #3621013 A step nobody can take raises an incident instead of a task nobody can see, and an audience says it restricts nobody by answering NULL

A human step whose audience matched nobody still became a work item: invisible, unclaimable, and the run stopped there with a log line for a signal. The only way out was an operator holding the reassign permission who happened to go looking.

Orchestra already has the object for a run that needs a person, and it carries exactly the resolutions this case wants. Retry re-queues the token so its node runs again, and running a human node again resolves its audience again, so correcting the audience and retrying is the repair. Resume writes corrected variables first, which is the repair for an audience that reads one, and is the case that had no route at all.

The engine asks before it parks

Parking is announced: it mints the work item and wakes the notification subscribers. A step that resolved to nobody must never get that far, so the audience is resolved before the park rather than after it. The token halts, the incident is raised, and no park event is emitted.

Resolution had to move out of WorkItemCreationSubscriber, which reacts to TokenParkedEvent at priority 100 precisely so the work item exists before any notifier runs. By then the token is PARKED and the decision sits in a subscriber with no business halting a token. The executor takes the matcher directly; it cannot take WorkItemManager, which would cycle back through ProcessControlInterface to the engine.

Three states, told apart by type

Both sentinel candidate values are gone. AssignmentMatcher::getCandidates() answers:

  • NULL from an audience that restricts nobody, which opens the step;
  • the resolved tokens;
  • an empty array for "I named people and matched none of them", which is invalid, never reaches storage, and is what the incident is raised on.

The three live on the return value rather than on a marker interface, because a marker only moves the problem: every caller asking a plugin directly would have to check for it too. The reassign timeout action resolves its escalation audience itself, and with a marker it read an Everyone escalation as "found nobody" and silently did nothing.

WorkItem::getCandidates() answers NULL for no rows, so both layers read the same way. The two SQL filters are untouched: IS NULL and NOT EXISTS already meant "no rows", which is now exactly the open case.

Nothing is stored that is not a real audience token.

What is deliberately unchanged

A work item created programmatically with no candidates is offered to everyone, as it always was. preSave() cannot tell that apart from a legitimate open step, since both are simply no rows, so the field description says it instead, where somebody writing one will read it.

This catches a step born unclaimable. A task whose role:editor audience later loses every member is frozen and still undetected, exactly as before; that wants a periodic sweep in InstanceRecovery and a per-dimension "could this token match anyone" question, which is its own change.

Follows #3620619.

Edited by Frank Mably

Merge request reports

Loading
Loading