Skip to content

reference: nexus integration contract — quirks Bombay MUST respect #125

Description

@joeldsouzax

The nexus integration contract for Bombay. nexus deliberately ships primitives, never a runner — the loop, dispatch, cursor, lifecycle, and supervision are all Bombay's. Every item below is a contract boundary where nexus stops and hands the problem to the runtime. Ignore one and it breaks subtly, not loudly.

Sources: nexus/CLAUDE.md + agency#137, #148–#155, #159, #160. (Some "depends on" refs in those cards point at upstream nexus tickets #127/#128/#129/#144/#146, not agency cards.)

AGGREGATE / CONCURRENCY

  1. Optimistic-concurrency conflicts are SURFACED, never retried internally. Repository::save / SagaRepository::react_and_save return VersionMismatch on a concurrent write; nexus does not loop (Rule 5: "no dependency on single-writer"). Bombay: provide single-writer-per-aggregate (actor-per-instance / sharding) OR implement explicit is_conflict() retry. Two writers on one stream silently lose events otherwise. (saga.rs, Rule 5; #149, #153)
  2. GlobalSeq is monotonic but NOT gapless — an aborted append burns values. Bombay: store last seen GlobalSeq, resume from: Some(seq) tolerating arbitrary gaps. Never gap-detect as "missing event". (store.rs)
  3. Version is 1-based NonZeroU64; replay enforces strict +1 starting at 1, with MAX_REHYDRATION_EVENTS (default 1M). Bombay: translate 0-based domains (KERI sn) at the boundary (version = sn+1) or keep sequence in the body; snapshot before the limit. (version.rs/aggregate.rs; core(message): Msg model + size-tripwire derive (#114) #137)
  4. AggregateState::apply is infallible; ALL validation lives in Handle::handle (decide), which has no access to version/identity/position. Bombay: push verification of externally-received events through a Handle<AcceptEvent{…}> command path; resolve seals/anchors at command-build time. Don't branch decisions on position. (aggregate.rs; core(message): Msg model + size-tripwire derive (#114) #137)
  5. SnapshotStore::commit(id, schema_version, position, &state) writes (state, position) atomically — a half-write is unrepresentable. This is the only atomicity nexus guarantees for derived state. Bombay: never persist checkpoint and read-model separately; snapshot writes are best-effort and must never block event persistence. (state.rs/snapshot.rs; #155)

SAGA

  1. Model A: intents are a 1:1 projection of the saga's OWN recorded events (intent_for), never a second independent output. Bombay: the dispatcher consumes ProjectedIntent tokens produced from saved events only — possessing one proves the source event is durable. (saga.rs; #150)
  2. ProjectedIntent is a capability token carrying (saga_id, source_version); outgoing intents are thin and un-enriched; at-least-once. Bombay: dispatch must enrich the thin intent into the target's fat command, dedup on (saga_id, source_version), and choose fanout policy. (saga.rs; #150, #154)
  3. Correlation EXTRACTION is pure (correlate(&E) -> Option<Key>); instance RESOLUTION is the runtime's. Two ignore levels: correlate → None (not routed) vs react → Ok(None) (routed, no-op) — don't collapse them. Bombay: build lookup-or-create instance resolution, single-writer-per-instance, lifecycle FSM, idle passivation. (saga.rs; #149)
  4. Two saga persistence paths, per-saga config. Own-stream sagas (have facts of their own, incl. timers) use Repository+replay, P = Version. Pure reactors use Projector+SnapshotStore, P = GlobalSeq. Bombay: support both, selectable per saga; any saga recording its own fact (timer/deadline) MUST be on the own-stream path or replay won't reproduce it. (store saga.rs; #148, #152)
  5. react_and_save is load-free core (react → atomic save → project intents); dispatch adds load for stateless reactors. No internal retry; intents pinned to assigned versions inside the save. Bombay: the forever-driver runs this per event, owns cursor/checkpoint, surfaces conflict (chore: fork kameo into bombay + workspace/CI setup #1), wraps with supervision. (store saga.rs; #148)
  6. Saga error/supervision policy (Skip / Retry{delay,max} / Stop, backoff, dead-letter, restart-on-crash) is the runtime's — nexus retired its FSM runner as loop bookkeeping. Bombay: implement via kameo supervision; restart-on-crash and dead-lettering are Bombay's alone. (#151, #155)

PROJECTION

  1. Projector::apply is FALLIBLE (unlike aggregate apply); recovery policy is the framework's. Bombay: runner owns skip / fail / dead-letter for projector errors — don't ignore the Result. (projection.rs)
  2. nexus gives at-least-once + the atomic commit primitive; exactly-once is the runner's to assemble. Bombay: fold projector + SnapshotStore::commit(state, position) transactionally; use PersistTrigger (EveryNEvents / AfterEventTypes) for when to commit. (projection/state; #155)
  3. Competing consumers / horizontal scale is runtime — nexus gives one cursor, not a partitioned one. Bombay: build the partition/lease scheme over the Zenoh actor framework. (#155)

SUBSCRIPTION / PASSIVATION

  1. The Subscription cursor NEVER returns None — caught-up means WAIT for new events (forever-driver). Bombay: drive inside a supervised/cancellable task with explicit shutdown; the loop owner decides when to stop. (subscription.rs)
  2. Three-layer notification split — nexus only provides the durable-cursor layer. In-process StreamNotifiers (notify/refill, re-read on caught-up) does NOT cross nodes. Bombay: layer Zenoh pub/sub for cross-node wakeups; treat re-read-on-caught-up as the correctness floor and the notifier as a latency optimization, not a delivery guarantee. (subscription_stream.rs; agency#138)
  3. from is exclusive (events strictly AFTER v); from: None = beginning. Bombay: persist last-processed position, resume Some(last), never re-feed last (combine with gap-tolerance refactor: ActorId identity — libp2p PeerId to Zenoh key-expr #2). (subscription.rs)

COMMAND BUS

  1. nexus ships NO command bus / routing / dispatch — only the pure Handle decide fn and the thin saga Command vocabulary (nexus#128 relocated to Agency). Bombay: build the bus on ZenohActorRef tell/ask with a composable interceptor chain (auth, retry, logging, validation); retry middleware keys off is_conflict() (chore: fork kameo into bombay + workspace/CI setup #1). (#153, #159)
  2. Command→handler registration must be compile-time exactly-one (mirrors Handle<C,N> one-impl-per-command; AggregateService::execute() currently unimplemented). Bombay: type-level (HList/Tower) registration guaranteeing exactly one handler per command. (#159; aggregate.rs)

CROSS-CUTTING

  1. nexus carries metadata as opaque Option<Bytes> and does NOT compute it (the generic M was deleted; it's IoT/no_std-first with no wall clock). Bombay: run an enricher at dispatch stamping correlation ID, the full causation chain, OTel trace/span IDs, timestamps/HLC into metadata — or audit trails are empty. (store.rs; #154)
  2. HLC field is nexus's, but HLC driving is the runtime's (nexus can't advance a clock). Bombay: drive the HLC in the sync loop, feed it through the metadata enricher. (#154, relates #146) — see Bombay feat: HLC timestamps — causal ordering / CRDT-friendly state #24
  3. The kernel is no_std / no_alloc / WASM-capable (Events<E,N> ArrayVec-backed, N=0 = single event no heap; IDs use ArrayString). Bombay: respect bounded Events<E,N> / ProjectedIntents<S,N> capacities (N+1); don't assume heap at the decide/react boundary; capacity hits return Result, not panic. This is what lets the same logic run on-device (lite-bombay devrandom-labs/bombay-sdk#7). (events.rs/id.rs, Rule 4)
  4. Each crate validates at its own boundary — do not trust upstream guarantees (store doesn't trust kernel's version>=1; fjall doesn't trust store). Bombay: the adapter/runner defends its own boundary (positions, schema versions, IDs) rather than assuming nexus did. (Rules 3/7)
  5. Test fixtures drive the REAL replay/decide path but there is NO controllable clock/stub scheduler yet. Bombay: supply the stub scheduler / TestClock for saga-deadline (#152) tests; time-dependent invariants can't ride nexus fixtures alone. (testing.rs; #160, #152)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions