Skip to content

sagas: engine ignores .correlate() extractor - all workflows collapse onto one shared instance #1066

Description

@rickylabs

Summary

The saga engine's instance resolution never consults the definition's .correlate()
extractor
. It keys instances on message.correlationKey ?? "<sagaId>:<messageType>".

Consequence: unless the publisher sets an explicit correlationKey on every message, all
logically distinct workflows collapse onto one shared saga instance. In the reproduction, every
incident in the system shared a single escalation saga.

.correlate() appears prominently in every example on the sagas capability page.

Found by a wave-four demo agent with no prior exposure to NetScript. Related: #1064, and the
sagaCompensate drop filed alongside this — same surface, same session.

Why this is the most dangerous of the three

The other two saga findings fail loudly enough to notice: one hangs, one produces no effect.
This one succeeds and produces wrong state. Two incidents raised minutes apart silently share
escalation level, deadline timers, and page history. Nothing errors. Nothing logs. The system looks
healthy and is corrupting workflow state.

It is also the hardest to catch in a demo or a test, because it only manifests once two
instances overlap — a single-workflow happy-path test passes cleanly.

Observed

Instance resolution uses:

message.correlationKey ?? `${sagaId}:${messageType}`

With no correlationKey on the published message, every IncidentTriggered maps to the same key,
so a second incident resumes the first incident's state machine rather than starting its own.

The agent found this by dumping KV keys directly — there is no error path that surfaces it.

Expected

Given a definition that declares an extractor:

defineSaga('escalation')
  .correlate((message) => message.payload.incidentId)
  ...

...the engine should use that extractor to derive the instance key, and only fall back to
correlationKey / the composite default when no extractor is declared.

Acceptance

  • The engine calls the definition's .correlate() extractor when one is declared, and uses its
    result as the instance key
  • Documented precedence for instance-key resolution (extractor → explicit correlationKey
    default), stated on the sagas capability page
  • A test asserts that two concurrent workflows with different correlation values get
    separate instances
    — the single-instance happy path does not cover this
  • If .correlate() is genuinely not wired yet, it fails loudly at definition-build time rather
    than being accepted and ignored, and the examples stop showing it as functional

Provenance

Wave-four demo run 1 (Claude Fable 5, high effort), 2026-08-03. ~20 min lost (combined with the
sagaCompensate finding); would have cost far more without a direct KV key dump.
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