Marconi
Marconi is the in-memory signal-mesh service that replacedsignal_service.py’s synchronous Redis-and-PG hot path in May 2026. Sender shim → Marconi → receiver shim. That is the whole signal flow. Everything else — durable cache, audit archive, OTEL — sits downstream of Marconi as an asynchronous fan-out and never blocks delivery.
If you want to understand how to use the signal mesh, read Signal Mesh first. This page is the architectural deep-dive: what Marconi owns, what tables it keeps, why Redis came off the hot path, and where the stage ladder stands today.
Source Of Authority
This page is the current Marconi signaling architecture source of authority for Prism. Historical specs and runbooks remain audit evidence, but this document is the system-of-record summary for current requirements and invariants. Normative record set:
When a historical spec conflicts with this page, treat the older text as migration history and update this page plus a new SPEC/ADR before implementation. Any client-facing signal path that does not enter through Marconi requires an explicit recorded reason and Frank approval.
The architectural lock
The architecture is locked by two operator quotes (Frank, 2026-05-10):“The signal flow is sender shim → Marconi → receiver shim. That is it. Marconi then pushes signal to Redis, then Redis pushes to PG. Redis is by no means in the signal flow.”
“Marconi switch in-mem → Marconi Cache 7d → PG audit. Once it hits cache it immediately goes to PG.”Three tiers, two flows, zero Redis on the signal-delivery path, zero MCP-to-PG shortcuts. The v0.4 topology is final for single-instance backends; cross-instance routing remains reserved (
MARCONI_CROSS_INSTANCE_FORWARD) for a future revision.
System Of Record
Marconi owns exactly one live signal state machine. The state machine is the system of record for recent signal activity:Gatekeeper Rule
All client-facing signal activity enters through the Marconi signal interface:
FastAPI HTTP routes may remain as compatibility adapters. They do not own signal state. They must delegate to Marconi-owned semantics and may consult
signal_queue, signal_trace_events, or other stores only as downstream projections or explicit historical/audit fallback.
Approved Store Roles
Approved Exceptions
The only approved non-primary paths are:- Historical reads with explicit
source=pg_auditorinclude_history=true. - Legacy fallback on Marconi Cache miss when the caller explicitly allows history.
- Migration bridges explicitly named in a SPEC/ADR, with owner, expiration condition, and tests.
- Operational diagnostics that read projections but never mutate signal state.
Current Requirements
Routing
- Routing is tenant-scoped first. Cross-tenant routing is denied before project or bridge checks.
- Project agents route to agents in the same project by default.
- Project agents may route to allowlisted bridge agents such as Sage, Clara, and Hazel-class bridge personas when the bridge contract authorizes it.
- Bridge agents may reply into a customer project only through an authorized bridge route, usually via
in_reply_toor explicit target context. - Name, identity, FQAI, and PNA fields are display/audit helpers unless the current routing spec says otherwise. Server-minted session state is the routing authority.
Real-Time Delivery
- Targeted delivery uses Marconi’s in-process routing and attached per-session queue/WS handle.
- Redis pubsub is retired from targeted signal delivery.
- Project broadcast uses Marconi fan-out where implemented. Org/tenant broadcast requires a recorded SPEC before adoption.
- Push-to-WS is transport evidence, not final model action evidence.
Pending And Drain
- Pending state is Marconi-owned. It may merge volatile in-memory pending entries, last-mile MCP buffers, and durable backstop rows, but the merge result is one Marconi-facing drain answer.
prism_signals_pendingmust not expose duplicate rows from multiple stores.- Draining a signal records delivery/observation evidence and may update downstream projections.
ACK And Observation
prism_signal_ackrecords model/surface observation. It is diagnostic evidence; it does not replace a required content reply.- ACK is cache-first. If
prism_signal_tracecan reporttrace_state=cache_accepted, ACK for the same trace must not returnTRACE_NOT_FOUND. - System-authored notices, including
signal_expired, must be ACKable without requiring a normal project-agentfrom_agent_idrow. - PG trace rows are projection evidence. PG lag or missing enrichment is not grounds to reject a recent cache-accepted ACK.
Trace And Audit
- Recent trace reads default to Marconi Cache.
- PG audit reads are explicit historical/reporting/reconciliation mode.
- Marconi Cache keeps approximately seven days of recent trace state.
- PG archiver consumes Marconi Cache and writes long-term audit rows idempotently.
- Audit fan-out failure is observable degradation; it does not block delivery.
Expiration, Recall, And Terminal State
- Expiration is a Marconi terminal-state transition and may emit a system
StatusUpdateto the original sender. - Recall must operate on the current terminal-state model. Already delivered, already expired, and not found are distinct outcomes.
- Replayed or redrained signals must preserve identity and trace correlation.
Observability
- The dashboard may read Marconi diagnostics, Marconi Cache metadata, and PG audit projections.
- Dashboard reads are observational only; they must not mutate signal state.
- Any disagreement between Marconi Cache and PG projection is an audit/projection lag or bug until proven otherwise, not proof that the Marconi signal is invalid.
Completion Checklist
A Marconi signaling change is complete only when:- The change enters through the Marconi interface or records an approved exception.
- Client-facing verbs do not introduce a second signal authority.
- Cache-first trace/read and ACK semantics agree.
- Targeted sends do not use Redis pubsub.
- PG writes are projection/audit, not hot-path authority.
- Bridge routing keeps tenant isolation and records cross-project context.
- Tests cover live/cache state and PG-lag/cache-only cases.
- Older docs touched by the change point back to this source-of-authority page.
What Marconi replaces
The legacy paths used Redis pubsub as an in-process message bus:- The sender thread did
PUBLISH session_channel - The recipient’s WebSocket handler did
SUBSCRIBE session_channel - A forwarding loop pumped messages from Redis pubsub onto the actual WebSocket
- A synchronous
INSERTintosignal_queueran on the hot path before the API returned 200
1736e40d made the drift visible.
Marconi eliminates the pubsub layer entirely. The receiver’s WebSocket handler registers itself with Marconi’s routing table at connect time (storing the WS handle or a per-session asyncio.Queue alongside the SessionRecord). The sender’s prism_signal looks up the recipient in Marconi’s routing table and pushes directly to the WS handle. No Redis publish, no Redis subscribe, no forwarding loop, no synchronous PG write.
Module path: backend/app/marconi/. Log prefix: marconi:. Metric namespace: marconi_*. The MCP verb surface (prism_signal, prism_signal_ack, prism_signal_trace, prism_signals_pending, prism_signal_recall) is unchanged — Marconi sits behind a stable external API.
In-memory tables (Marconi-owned)
All tables are per-process, hash-keyed, and protected byasyncio.Lock.
Routing table
Lookup is O(1). Updated on
register, deregister, heartbeat, WS connect, WS disconnect. Read on every send_signal.
Registration table
Sister index of routing; allows session-id-keyed lookups for heartbeat, deregister, and WS-attach.
WS handle storage
Each Marconi entry holds either the liveWebSocket object directly OR a per-session asyncio.Queue that the WebSocket handler reads from. When send_signal resolves a recipient, it:
- Fetches the entry from the routing table.
- Pushes the signal envelope into the entry’s WS handle / queue.
- Returns 200 to the sender.
outcome=queued_offline is recorded.
Obligation index
Per-tenant in-memory map of open obligations:
In-memory primary; the Redis Stream handoff reflects
durability_status. SLA sweepers operate on the in-memory map.
Pending-signal index
Per-recipient queue of undelivered signals + TTL. Drained on recipient registration / WS connect, and on explicitprism_signals_pending calls. Entries reference audit-queue rows by id.
In-memory audit queue
Internal Marconi buffer that feeds the audit fan-out. Not on the delivery path. Append-only queue pertenant_id holding accepted signal envelopes between the moment send_signal returns 200 and the moment the Redis Stream writer successfully appends the entry to marconi:signals:{tenant_id}. Each entry is a full signal envelope plus arrival timestamp plus delivery outcome (pushed, queued_offline, no_subscriber, etc.).
Sizing — per-tenant config:
Sizing formula:
max_entries ≥ burst_rate_per_sec × max_redis_writer_lag_seconds × safety_factor (≥2), bounded by max_bytes.
Defaults (personal install): max_entries=50_000, max_bytes=512MB, max_age_seconds=600, overwrite_policy=oldest. Production multi-tenant tuning requires observed lag/overwrite metrics for at least one steady-state day.
Marconi does not block on the Redis writer’s health. When the writer is unreachable, the audit queue grows; when the queue fills, the oldest entries are overwritten and counted as marconi_audit_queue_overwrite_total — the operator-visible loss event under v0.4’s loss budget.
Cache invalidation — direct write-through, no pubsub
All session-state changes happen inside the same Python process as Marconi’s tables. Invalidation is a direct function call. No Redis pubsub anywhere in Marconi’s surface. The canonical hook table:marconi/lifecycle.py provides warm_caches_from_store(store) called from the FastAPI lifespan after SessionStore connect. Every hook above has a unit test covering the cache state transition; marconi_invalidator_errors_total must be zero in steady state.
Audit fan-out
Three FastAPI in-process background tasks consume the audit queue downstream of the hot path:- Redis Stream writer — drains the in-memory audit queue into
marconi:signals:{tenant_id}Redis Stream withMAXLEN ~7d(approximate trim by time). This stream is the Marconi Cache and the durable boundary for v0.4. Writer resumes from last-acknowledged audit-queue offset (held in Redis as a key for restart safety). Every successfulXADDwakes the PG archiver’s blocking consumer-group read. - PG archiver — consumes the Marconi Cache as a consumer group with
XREADGROUP COUNT 1 BLOCK 0. Each stream entry drains near-immediately into PG audit with idempotentsignal_idUPSERT semantics againstsignal_queue,signal_trace_events, andsignal_obligations. No batching. The archiver optimizes for near-immediate audit persistence, not throughput batching. - OTEL emitter — increments
marconi_signals_received_total,marconi_signals_delivered_total, etc. Background; never blocks.
Stage ladder — current shipped status
The migration landed as a sequence of feature-flagged PRs. Each stage cleared exit criteria before the next stage’s flag flipped. Flag flips, in shipping order:
Stage 5 could not ship until Stage 4 archiver was primary — otherwise accepted signals would be delivered but not recorded. The dead-branch cleanup in PR #301 removed
AUDIT_PENDING_MARCONI and ACK_DEFERRED_MARCONI paths that the cutover obsoleted. PR #300 normalized the publish_path value from internal pushed_to_marconi to external pushed_to_ws at the archiver boundary.
Additional shipping work:
- Sign-coded result envelope (PR #285, #286) —
[stage=…]banner the shim renders alongside the doorbell to make the delivery stage observable without a separate trace call. - Channel-probe verbs (PR #287, #290) —
prism_channel_probeandprism_channel_probe_ackfor operator-invoked end-to-end loopback diagnostics. Default ACK timeout bumped 5s → 10s in PR #290 to accommodate per-persona daemon turn-boundary holds.
Counters — full §9 catalog
Every metric below must exist; the Stage 5 hot-path cutover required them in place before the flag flip.Hot-path counters (per tenant)
marconi_signals_received_total{tenant,signal_type}— at API entrymarconi_signals_delivered_total{tenant,signal_type,outcome}— outcome ∈pushed,queued_offline,no_subscriber,droppedmarconi_signals_acked_total{tenant,signal_type,ack_kind}— final-delivery evidencemarconi_signal_send_duration_seconds{tenant}— histogram, p50/p95/p99marconi_routing_table_size{tenant}— gaugemarconi_routing_cache_hits_total{tenant}/marconi_routing_cache_misses_total{tenant}
Audit queue + fan-out counters
marconi_audit_queue_depth{tenant}— gaugemarconi_audit_queue_overwrite_total{tenant}— counter (loss event; non-zero is a paging incident)marconi_redis_writer_lag_seconds{tenant}— gaugemarconi_redis_writer_errors_total{tenant,reason}— countermarconi_pg_archiver_lag_seconds{tenant}— gaugemarconi_pg_archiver_errors_total{tenant,reason}— counter
Cache-invalidator counters (Stage 2)
marconi_invalidator_calls_total{hook}— counter (hook ∈on_register,on_deregister,on_master_change,on_heartbeat,on_session_expired,on_ws_connect,on_ws_disconnect)marconi_invalidator_errors_total{hook,reason}— counter (non-fatal hook errors; non-zero in steady state is a paging incident)
Obligation counters
marconi_obligations_open{tenant,kind}— gaugemarconi_obligations_durable_total{tenant,kind}— countermarconi_obligations_degraded_not_durable_total{tenant,kind}— countermarconi_obligation_sla_violation_total{tenant,kind,sla}— counter
marconi_* namespace via /metrics; see Metrics for the full Prism counter catalog and dashboard wiring.
Failure modes (summary)
Full table in the SPEC-101 loss budget. The short version: delivery is the contract; durability is best-effort with a bounded, observable loss surface.
Cold-start window after process restart: ~1–2 seconds where new signals route to
outcome=queued_offline while shims reconnect. Acceptable for restart cadence; routed signals land in the audit queue and reach the recipient on the next push or via startup drain.
Future work (carrying beyond v0.4)
- Cross-tenant isolation cost analysis at production multi-tenant scale. v0.4 sizes for personal install; production multi-tenant tuning requires observed steady-state lag/overwrite metrics.
- Multi-process cross-instance routing. When backend instances scale out, cross-instance routing becomes necessary. Either sticky-partitioned routing by tenant or pubsub-driven invalidation across instances. Out of scope for v0.4;
MARCONI_CROSS_INSTANCE_FORWARDremains reserved. - Durability-recovery algorithms for the “delivered but not recorded” set when Redis is down longer than the audit queue holds. Currently surfaced as an operator alert (
marconi_audit_queue_overwrite_total) without an automatic recovery path. - Per-call
durable=trueopt-in. Some callers may require synchronous-on-return durability (prism_postmortem,prism_decide, etc.). Already separate verbs, mostly already PG-direct through FastAPI. Per-call flag proposed; out of scope for v0.4.
References
- SPEC-101 v0.4 — canonical spec
- SPEC-101 loss budget + recovery — Stage 0 gate: loss budget, recovery invariants, rollback procedures
- SPEC-100 — operator-invoked signal-mesh loopback probe (
prism_channel_probe) - Marconi disaster-recovery runbook — operational steps for archiver lag, Redis outage, audit-queue overwrite incidents
- ADR-56 — locks the MUST and the rename
- Postmortem
1736e40d— drift root-cause that motivated the rewrite - Texi ratification chain — signals
37140b98→2e1d6a4e→4302012d→8620d2f5 - Frank operator architecture lock (2026-05-10) and v0.4 GO (2026-05-10 23:30Z)

