You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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
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)
GlobalSeq is monotonic but NOT gapless — an aborted append burns values. Bombay: store last seenGlobalSeq, resume from: Some(seq) tolerating arbitrary gaps. Never gap-detect as "missing event". (store.rs)
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)
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)
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
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)
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)
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)
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)
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)
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
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)
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)
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
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)
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)
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
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)
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
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)
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
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)
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)
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)
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
Repository::save/SagaRepository::react_and_savereturnVersionMismatchon 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 explicitis_conflict()retry. Two writers on one stream silently lose events otherwise. (saga.rs, Rule 5; #149, #153)GlobalSeqis monotonic but NOT gapless — an aborted append burns values. Bombay: store last seenGlobalSeq, resumefrom: Some(seq)tolerating arbitrary gaps. Never gap-detect as "missing event". (store.rs)Versionis 1-basedNonZeroU64; replay enforces strict +1 starting at 1, withMAX_REHYDRATION_EVENTS(default 1M). Bombay: translate 0-based domains (KERIsn) 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)AggregateState::applyis infallible; ALL validation lives inHandle::handle(decide), which has no access to version/identity/position. Bombay: push verification of externally-received events through aHandle<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)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
intent_for), never a second independent output. Bombay: the dispatcher consumesProjectedIntenttokens produced from saved events only — possessing one proves the source event is durable. (saga.rs; #150)ProjectedIntentis 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)correlate(&E) -> Option<Key>); instance RESOLUTION is the runtime's. Two ignore levels:correlate → None(not routed) vsreact → 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)Repository+replay,P = Version. Pure reactors useProjector+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)react_and_saveis load-free core (react → atomic save → project intents);dispatchadds 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)PROJECTION
Projector::applyis FALLIBLE (unlike aggregate apply); recovery policy is the framework's. Bombay: runner owns skip / fail / dead-letter for projector errors — don't ignore theResult. (projection.rs)SnapshotStore::commit(state, position)transactionally; usePersistTrigger(EveryNEvents/AfterEventTypes) for when to commit. (projection/state; #155)SUBSCRIPTION / PASSIVATION
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)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)fromis exclusive (events strictly AFTERv);from: None= beginning. Bombay: persist last-processed position, resumeSome(last), never re-feedlast(combine with gap-tolerance refactor: ActorId identity — libp2p PeerId to Zenoh key-expr #2). (subscription.rs)COMMAND BUS
Handledecide fn and the thin sagaCommandvocabulary (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 offis_conflict()(chore: fork kameo into bombay + workspace/CI setup #1). (#153, #159)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
Option<Bytes>and does NOT compute it (the genericMwas 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)no_std/no_alloc/ WASM-capable (Events<E,N>ArrayVec-backed, N=0 = single event no heap; IDs useArrayString). Bombay: respect boundedEvents<E,N>/ProjectedIntents<S,N>capacities (N+1); don't assume heap at the decide/react boundary; capacity hits returnResult, 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)