# `orchestrator/compensate-runner` ## Purpose `runFullCompensation(...)` is the orchestrator's per-instance compensation driver. It bridges the generic `compensation` module (which only knows about `CompensableStep` + retry loop) with the saga-specific concerns: looking up each completed step's registered compensate handler, transitioning the step through `compensating → compensated`, recording the compensation counter + span, and emitting `saga.step.compensated`. The result of the walk is returned as a structured outcome so the orchestrator can settle the instance state. This module is split out from `orchestrator.ts` to keep the main file under the ~250-line soft limit and to isolate the compensation concerns from the forward step-dispatch loop. ## Features - Walks completed step rows in reverse via the generic `runCompensation` helper. - Per-step: opens a `saga.compensate` span, transitions to `compensating`, invokes the registered compensate handler under a fresh `RequestContext`, increments `saga.compensations_total`, transitions to `compensated`, emits `saga.step.compensated`. - Honours per-saga `maxCompensationAttempts` (defaults to 5 via the `compensation` module). - On terminal failure: transitions the failed step row back to `failed` so the persistence layer reflects the broken state. ## Usage: Basic Internal helper — typically only consumed by `createSagaOrchestrator`: ```ts import { runFullCompensation } from "@suaveplan/api-saga/orchestrator/compensate-runner"; const result = await runFullCompensation({ instance, maxCompensationAttempts: 5, principal, persistence, registry, emitter, instruments, requestContextFactory, nowMs: () => Date.now(), }); if (result.outcome === "compensated") { persistence.transitionInstance(instance.id, "compensated"); } ``` ## Usage: Advanced A consumer that wants to drive compensation manually (e.g. an operator-triggered "rollback this saga" admin command) can call this directly without going through `start()` or `resume()`. The caller is responsible for transitioning the instance into the matching terminal state afterwards: ```ts import { runFullCompensation } from "@suaveplan/api-saga"; const result = await runFullCompensation({ instance: persistence.requireInstance(id), maxCompensationAttempts: 3, principal: null, persistence, registry, emitter, instruments, requestContextFactory, nowMs: () => Date.now(), }); persistence.transitionInstance( id, result.outcome === "compensated" ? "compensated" : "compensation_failed" ); ``` ## API Reference - `runFullCompensation(args): Promise`. - `args`: - `instance: SagaInstanceRow`, - `maxCompensationAttempts: number`, - `principal: Principal | null`, - `persistence, registry, emitter, instruments, requestContextFactory, nowMs` — injected dependencies. - `CompensationFlowOutcome`: - `{ outcome: "compensated" }`, - `{ outcome: "compensation_failed", failedStep: string }`. ## Implementation Notes The compensation walk only considers step rows whose persisted state is `completed`. Steps in `pending`, `in_flight`, or `failed` have either not run or already failed; running compensation against them would either invoke a compensate handler with no matching output or repeat work the runner already declared lost. `updateStepPayload` is invoked AFTER the compensate handler so the persistence layer records the final attempt count rather than the initial one. This matters for the operator-facing dashboards that read the row to display "compensation took N attempts".