Skip to content

sagas: sagaCompensate effects are silently dropped - createDurableSagaRuntime wires no compensator by default #1065

Description

@rickylabs

Summary

A saga handler that returns sagaCompensate(...) produces no error, no log, and no
compensation
. The effect is silently discarded by the runtime that createDurableSagaRuntime
builds.

Compensation is the mechanism the "a multi-step operation must not leave the world half-changed"
guarantee rests on, and the sagas capability page presents .compensate() + sagaCompensate() as
a live, first-class path with no warning that it requires additional wiring.

Found by a wave-four demo agent with no prior exposure to NetScript. Related: #1064 (same surface,
same session).

Reproduction

Saga definition registers both the failure handler and a compensation branch:

.on('PageDeliveryFailed', (state, message) => [
  sagaCompensate({ level: state.level, pages: state.pages }, 'delivery failed mid-level'),
])
.compensate('PageDeliveryFailed', (state) => /* void the half-sent pages, escalate */)

Then signal it:

curl -X POST .../escalation/signal -d '{"type":"PageDeliveryFailed", ...}'

Observed

job result:   {"delivered": false, "voided": 1, "reroutedToLevel": 2}   # decision.level stayed 2
saga history: [triggered, level-paged, deadline-elapsed, escalated]     # no level-voided, no escalation

The compensation branch never ran. Nothing was logged. Nothing errored. The sagaCompensate
effect simply evaporated between the handler returning it and the runtime processing the effect
ledger.

Root cause

The runtime built by createDurableSagaRuntime wires no compensator by default
SagaBusBridgeOptions.compensator is optional, and when it is absent a returned sagaCompensate
effect is dropped rather than rejected.

So the default runtime silently does not support a documented first-class effect.

Why this is worse than a normal missing feature

The failure mode is invisible in every direction:

  • The handler returns successfully.
  • The effect ledger is accepted.
  • No warning is emitted that an effect kind is unhandled.
  • Saga history simply lacks the compensation entries, which reads as "the branch didn't trigger"
    rather than "the runtime ignored it".

An author following the capability page has no signal that their compensation logic is dead code.

Workaround used

Moved the undo logic into a plain .on('PageDeliveryFailed') failure branch. Note the storefront
tutorial does recommend that shape — so the docs contain two paths, one of which works and one of
which silently does not, with nothing marking the difference.

Acceptance

  • createDurableSagaRuntime wires a working compensator by default, so a returned
    sagaCompensate(...) runs the registered .compensate() branch
  • Or, if compensation genuinely requires opt-in wiring: an unhandled effect kind fails
    loudly (throw or error-log naming the effect and the missing option) rather than being
    dropped, and the capability page documents the requirement at the point .compensate() is
    introduced
  • A test asserts that a sagaCompensate effect from a handler actually invokes the registered
    compensation branch through the default runtime
  • The sagas capability page and the storefront tutorial agree on which path is supported

Provenance

Wave-four demo run 1 (Claude Fable 5, high effort), 2026-08-03. ~20 min lost (combined with the
.correlate() finding filed separately).
Full drift log: /home/codex/repos/wave4-fable/.llm/run/drift.md, finding #3 of 9.
Product repo: https://github.com/rickylabs/vigil

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions