Follow-up to the poison-token dead-letter work (merge request !163, audit finding #13).
Background
The dead-letter policy moves a poison token (one whose task throws on every retry) to a terminal STATUS_ERROR after a bounded number of attempts and then fails the whole instance, so a misbehaving task can no longer retry forever or wedge the process.
Limitation
Dead-lettering always fails the entire instance. There is no way for an operator to inspect the failed token and recover it: retry it, skip the node, edit a variable and resume, or cancel just that branch.
Proposed
Add an "incident" model (compare Camunda incidents): when a token dead-letters, raise an incident and park the token in ERROR without necessarily failing the instance, and offer operator actions to retry (reset the attempt counter and re-enqueue), skip, resume, or cancel. Surface incidents in orchestra_ui, orchestra_views and the inbox. This likely pairs with per-node retry configuration (max attempts, backoff).
Note: STATUS_ERROR was introduced distinct from CANCELLED specifically so it can become this "needs attention" state.
Issue fork orchestra-3604207
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 #2
mably commentedConcrete kernel-level implementation plan, building on the dead-letter primitives already shipped (token STATUS_ERROR, the per-token attempts counter, WorkflowEngine::advanceQueued() and handleAdvanceFailure()).
Design decision
The instance stays RUNNING while incidents are open; "in incident" is driven by open incident records, not a hard status that freezes healthy branches. Other branches keep advancing, so advance()'s isRunning() guard is unchanged. A derived STATUS_INCIDENT may be added for display only.
New state
New kernel entity orchestra_incident
Execution state, so kernel-owned (its own content entity, not fields on ProcessInstance): instance ref, token ref, node_id, message, attempts, status (open|resolved), resolution (retried|skipped|resumed|cancelled|failed), resolved_by, created/changed. Deleting an instance deletes its incidents, mirroring InstanceCleanup.
Engine: branch handleAdvanceFailure at the cutoff
raiseIncident() creates the incident row and emits an audit event, but does NOT call cancelLiveTokens(), so the other branches keep running. Policy comes from an orchestra.settings key on_unrecoverable_failure (incident|fail, default incident), overridable per node.
checkCompletion respects open incidents
Completion now requires zero live tokens AND zero open incidents, so an instance with a stuck branch does not wrongly complete; it halts awaiting resolution.
Recovery API (new WorkflowEngineInterface methods)
Operator-driven, behind a new "resolve orchestra incidents" permission:
All reuse existing primitives (enqueue, setVariable, cancel, failInstance) except skip.
The one refactor: skipNode
advanceNode() is join then task then split. Extract the successor-production tail into produceSuccessors(); advanceNode() calls it after the task, skipIncident() calls it directly (in the same transaction advance() uses). Split/flow-condition/fork logic is unchanged.
Supporting
Migration and BC
New orchestra_incident table and a new permission. Pre-1.0 ships no update hook (fresh installs get it; live sites via the throwaway entity-update); post-1.0 it would ship a real update hook. Adding methods to the @api WorkflowEngineInterface is safe for consumers but breaks a third-party decorator implementing it; note in the changelog if it lands post-1.0.
Comment #3
mably commentedComment #8
mably commented