DD-006: Events DSL (hless, stream, event_model, subscribe, projection, ingest), the Kafka bus and the event-transport residue — parked until a consumer needs an ordered or replayable stream, live push, or a Redis-free bus¶
id: DD-006
status: PARKED
issues: [1069, 1189, 1676, 1399]
adrs: [0057, 0047, 0017, 0003]
decided: 2026-10-12
decided_at_sha: 2de583aa741ecc4dfad1f369f6c773fa12f99966
parent: flense-programme
One-line intent¶
No consumer app declares an event construct. Flense ship F (ADR-0057) removes
the event vocabulary of the DSL and the Kafka bus adapter, and keeps the
event core every consumer already runs: EventsSubsystem, the
transactional outbox and publisher, the inbox, and the Redis bus. (Flense
ship O corrected this list: EventEmittingMixin was never mixed into a CRUD
service and was removed — see Already shipped. Flense ship AR parked the
transport nobody selects — the Postgres and in-memory buses, the event
explorer, SSE live push and the channel delivery outbox — see Runtime
transport below.)
Plan (what we already concluded)¶
Restore from the commit this DD was parked at
(2de583aa741ecc4dfad1f369f6c773fa12f99966), trimmed to what the forcing
consumer needs:
- Parser mixins
dsl_parser_impl/{hless,eventing,ingest}.py, IRcore/ir/{hless,hless_validator,eventing,ingest,llm_events}.py, thevalidate_event_payload_secrets/validate_ingestsvalidators, the stream half ofvalidate_money_fields, the guide-concordance check oftopic.eventcompletion refs, the entitypublishdirective, and the lexer keywords they used (event_model,publish,subscribe,projection,topic,hless,strict,partition_key,ordering_scope,idempotency,derives_from,t_event, …). - Runtime:
http/runtime/ingest_{engine,routes}.py(POST /api/ingest/{name}, the_dazzle_ingest_fingerprinttable — dropped by revision 0022 after operator sign-off on 2026-10-13; re-add it with a new Alembic revision),http/events/topology_drift.pyandmulti_tenancy.py,http/events/kafka_bus.pyplus thekafkatier intier.py, and thesemantics validate_eventsMCP op. - Design points to keep: HLESS record kinds (INTENT / FACT / OBSERVATION /
DERIVATION) with mandatory partition key, ordering scope and idempotency
strategy per stream; money in FACT/INTENT payloads must be the
moneytype; no secret-named fields in payloads. The namehlessis deliberate (#1069 API-003 rejected renaming it). - Counter-priors: do not let app code publish ad-hoc topics outside a declared stream; do not reintroduce a broker tier before Postgres/Redis are shown insufficient for the consumer's volume.
Reopen when (consumer force)¶
- Named consumer — pennydreadful rights auctions need an ordered or replayable bid stream (bids ordered per lot, replay to rebuild the auction state).
- Evidence of host workaround — a consumer hand-rolling a bid log table with sequence numbers, or replay scripts over the outbox.
- Acceptance that unblocks — the consumer's stream validates, bids are delivered in order per partition key, and a projection rebuilds from replay in that consumer's CI.
When a force lands:
- Set
status: FORCEDand date it. - Comment on every linked issue with the consumer + this DD path.
- Implement against the plan above; open child issues if the plan splits.
- When shipped, set
status: DONEand close issues.
Already shipped (do not re-litigate)¶
- The event core stays and starts for every consumer:
EventsSubsystembuilds theEventFramework(bus tier, outbox publisher, inbox, consumer groups). Thedazzle events/dlq/outboxCLI groups were retired in flense ship J and the/_dazzle/events/*explorer in flense ship AR — no consumer ran either. What actually travels on it today: - CRUD lifecycle events do not use the bus or the outbox.
CRUDServicelifecycle callbacks feed the in-processEntityEventBus(SystemRoutesSubsystem._wire_entity_events_to_bus), whose handlers are integration mappings, LLM triggers, SLA timers and app handlers (cyfuture registersadd_sync_handlerfor lifecycle and HMRC work). (Until flense ship AR,sse_wiringalso published a nudge envelope forlive: onworkspaces; see Runtime transport.) - Auth events (
auth.user.registeredand siblings) are published straight to the bus byauth/events.py; cyfuture consumes them throughEventFramework.register_handler. - The process event-bus adapter publishes its own envelopes to the bus.
- The transactional outbox has no framework writer. Its only writer
was
EventEmittingMixin, which no CRUD service ever inherited (theset_event_frameworkwiring inEventsSubsystemnever matched a service); flense ship O removed the mixin and that wiring. The outbox table,EventFramework.emit_event(the public append API) and the publisher that drains it stay as event core. A forcing consumer that needs transactional CRUD events needs the append inside the repository write transaction — theCRUDServicelifecycle callbacks fire after the write, and the removed mixin handedemit_eventthe repository's backend object rather than the open transaction, so neither was transactional (mixin restore point:src/dazzle/http/events/service_mixin.pyat7879465836d4c38ded550e9c564f1a7d7d29d9e8). - AegisMark's production boot log confirms the Redis Streams bus
(
REDIS_URLset), so the Redis tier is core, not parked.
Runtime transport (parked 2026-10-16, flense ship AR)¶
Operator decision 2026-10-16 (flense plan §10.3/§10.4 item 1): park the
event-transport residue, keep the Redis path cyfuture runs in production.
Restore from commit 5bba9833f5eec7decf209059c8dbbbf0afeded73 (main
before ship AR); git show 5bba9833f:<path> returns every file below.
Removed.
src/dazzle/http/events/postgres_bus.py(919 lines):PostgresBus/PostgresConfig(a LISTEN/NOTIFY bus with self-created{prefix}events,{prefix}consumer_offsetsand{prefix}dlqtables), itsasyncpg-gated export inhttp/events/__init__.py, thepostgrestier intier.pyand theevent_bus_transportclass of the DB-artifact registry. No consumer deployment selected it: cyfuture and AegisMark run withREDIS_URL, and nothing setsEVENT_BACKEND.EventFrameworkpicked it wheneverredis_urlwas empty, so local runs without Redis used it.src/dazzle/http/events/dev_memory.py(373):DevBusMemory, thememorytier and theEVENT_BACKENDoverride. Tests usetests/unit/helpers/event_bus.py::RecordingBus.src/dazzle/http/runtime/event_explorer.py(552): the/_dazzle/events/*routes (status,topics,topics/{topic},event/{id},consumers,outbox,dlq,POST dlq/{event_id}/replay). They were mounted for every app with a database and had no auth dependency (the observability guide listed their gating as "None (all environments)"), so event payloads such asauth.user.registered(email, username, roles) were readable and DLQ replay was callable without signing in. Also gone: theEventFramework/NullEventFrameworkget_outbox_statsandget_recent_outbox_entrieshelpers only the explorer called.- SSE live push (#1399 slice 1):
http/runtime/sse_stream.py(339),http/runtime/sse_wiring.py(150), the/_ops/sse/eventsroute,DazzleBackendApp._wire_sse_live_push,WorkspaceSpec.liveand the parser'slive:state,WorkspaceContext.sse_url,DashboardGrid.sse_urlandDashboardCard.sse_enabled(thehx-ext="sse" sse-connectattributes and thesse:entity.*card triggers). No consumer DSL declareslive: on. The browser never connected anyway:htmx-ext-sse.jsis not vendored and the vendored htmx 4 core has noEventSourcesupport, so cards refreshed only by poll.live: <value>still parses and warnsW_DECLARED_NOT_READ. http/channels/outbox.pyOutboxRepositoryandensure_outbox_table(the channel delivery outbox, table_dazzle_outbox): never constructed (flense ship AI).OutboxMessage,OutboxStatusandcreate_outbox_messagestay as the email adapters' message type.
Schema. Alembic revision 0025_flense_park_event_transport drops
_dazzle_outbox (no writer existed, so it is empty on every consumer
database); downgrade recreates it with the removed DDL. The registry, the
framework schema orchestrator and FRAMEWORK_SCHEMA_SNAPSHOT follow (23 → 22
in-scope tables). Operator sign-off for the drop: 2026-10-16. The
PostgresBus transport tables (_dazzle_events, _dazzle_consumer_offsets,
_dazzle_dlq under the default prefix) were never migration-managed; a
database that booted without REDIS_URL may still hold them. They are not
dropped (no sign-off; Alembic autogenerate ignores the _dazzle_ prefix).
Kept (the consumer path).
RedisBus(redis_bus.py),BaseEventBus,EventBus,EventEnvelope,NullBus/NullEventFramework,EventFramework,OutboxPublisherwith the_dazzle_event_outboxtable,IdempotentConsumer,EventsSubsystem,auth/events.py(theauth.user.*envelopes),EventBusProcessAdapterand the in-processEntityEventBus.- The inbox stays. The flense plan proposed dropping
inbox.pyand_dazzle_event_inbox. Tracing showed thatEventFramework._start_consumerswraps everyregister_handlersubscription in anIdempotentConsumerbacked byEventInbox, and cyfuture registersauth.user.registered→cyfuture-provision(services/auth_event_handlers.py). Dropping it would break de-duplication on cyfuture's production path. - Without
REDIS_URLthe bus is now the null tier (EventTier.NULL,NullBus): publishes are dropped and registered handlers never fire. Before ship AR the same setup ran the Postgres bus. Process execution without Redis is unchanged (PostgresProcessAdapter, not the bus).
Reopen when (consumer force).
- A consumer declares
live: onon a workspace and needs push rather thanrefresh: every Ns. Restoresse_stream.py/sse_wiring.py, theliveIR field and the renderer attributes, and vendor the htmx SSE extension (or an HM controller usingEventSource), keeping the/_ops/sse/eventsauth posture from flense ship AO (tenant from the session, never the query string). - A consumer must deploy without Redis (Postgres only) but needs event
delivery (auth-event handlers, process events across workers). Restore
postgres_bus.pyand its tier; register its transport tables in the artifact registry (ADR-0047) and create them by migration (ADR-0017) instead of self-creation. - An operator needs HTTP event inspection. Restore the explorer behind an admin auth dependency, never unauthenticated.
Out of scope until force (transport). A durable delivery outbox for email (the messaging DSL is DD-007), WebSockets, cross-tenant event inspection.
Explicitly out of scope until force¶
- Kafka, EventBridge, schema registries, cross-tenant topic namespaces.
Trail¶
| Date | Event |
|---|---|
| 2026-10-12 | Parked (this DD); events DSL and Kafka bus removed in flense ship F |
| 2026-10-13 | Flense ship O: Already shipped corrected — CRUD events never reached the outbox; unused EventEmittingMixin removed |
| 2026-10-16 | Flense ship AR: Postgres and in-memory buses, the /_dazzle/events/* explorer, SSE live push (live: now warns) and the channel OutboxRepository removed; _dazzle_outbox dropped by 0025_flense_park_event_transport; the inbox kept (cyfuture's handler path) |