Expand description
v0.11.0 P4 — bridge per-tenant InvalidateEvent broadcasts into
MCP notifications/message events on each session’s SSE stream.
§Scope
v0.11.0 ships only the two MCP-spec-defined notification shapes
(notifications/progress and notifications/message) per plan §3
Decision B. P3 wired notifications/progress (per-tool progress).
P4 wires notifications/message by subscribing to each tenant’s
existing TenantHandle::invalidate_sender() broadcast channel (the
same channel that powers /v1/graph/stream for solo-web) and mapping
each InvalidateEvent into a JSON-RPC notifications/message
envelope on the per-session broadcast channel from P1+P2.
§Mapping policy (locked for v0.11.0)
InvalidateEvent carries a kind discriminator (episode,
document, chunk, cluster, triple, tenant) and a reason
string (memory.remember, memory.forget, …) per
docs/dev-log/0118-graph-stream-impl.md. P4 maps these conservatively
into MCP data strings the client can switch on:
kind | data string | Notes |
|---|---|---|
episode | memories_updated | Coarse signal: any episode-level change |
document | documents_updated | Ingest / forget at document granularity |
chunk | documents_updated | Coalesced under documents (chunks are a leaf of one document) |
cluster | consolidation_updated | Steward consolidation landed |
triple | graph_updated | Triples-extract committed |
tenant | tenant_updated | GDPR cascade or tenant lifecycle |
| other | memory_updated | Fallback so a future writer kind never silently drops |
level = "info" for every variant; logger = "solo". The MCP spec’s
notifications/message shape allows level ∈ {debug, info, warning,
error}; v0.11.0 emits at info because every wire shape is a
committed write (invariant: InvalidateEvent is only sent after the
writer-actor’s commit succeeds — see solo_core::InvalidateEvent’s
docstring).
Each emitted MCP message ALSO carries the original reason,
tenant_id, and ts_ms in a nested details object so clients that
want the structured shape get it without losing the coarse
data switch. The two halves stay in lock-step — data is what to
refetch; details.reason is why.
§Per-session task lifecycle
For each session created by POST /mcp, the POST handler calls
spawn_invalidate_bridge passing the freshly-resolved
Arc<TenantHandle> + Arc<SessionState>. The function:
- Calls
tenant.invalidate_sender().subscribe()BEFORE downgrading the session Arc to aWeak<SessionState>. - Spawns a
tokio::spawntask that loops onrx.recv():Ok(event)→ map viamap_invalidate_to_message, callsession.publish_event(Message, envelope)via theWeakupgrade. Returns immediately if upgrade fails (session dropped — task exits).Err(RecvError::Lagged(n))→ log warn (matches/v1/graph/streamdiscipline) and continue. The session’s own event-channel lag handling kicks in on the GET side; the bridge itself is best-effort fan-out.Err(RecvError::Closed)→ tenant handle dropped (rare outside test shutdown); task exits cleanly.
The task holds NO strong reference to SessionState — only a
Weak. The session’s Arc lives in the [SessionStore]; when the
store drops the session (TTL expiry or delete), the Weak upgrade
fails on the next event and the task exits. This is the
session_task_exits_when_session_dropped test contract.
§Out of scope (v0.12.0+)
- Coalescing rapid bursts. v0.11.0 emits one MCP message per
InvalidateEvent. A pathological pattern (e.g. 1000 rapidmemory.remembercalls under one session) would emit 1000 messages. The per-session broadcast channel is bounded (crate::mcp_session::MCP_SESSION_EVENT_BUFFER_CAPACITY= 256), so a slow GET subscriber would seeevent: laggedper P2 rather than memory growth. A future v0.12.0 follow-up could coalesce bursts at the bridge tier (e.g. “if N events of the same kind in 1s, emit one”); not needed for v0.11.0 launch. - Solo-custom event types (
audit_event,memory_consolidated,tenant_changedas Solo-custom shapes). Deferred to v0.12.0+ per plan §3 Decision B. P4 ships only spec-compliantnotifications/message. - Per-event auth scoping. The bridge inherits the session’s
bound tenant; cross-tenant filtering happens at the broadcast
layer (each tenant has its own
invalidate_sender) so cross- tenant leakage is impossible by construction.
Constants§
- MCP_
NOTIFICATION_ DATA_ CONSOLIDATION_ UPDATED datadiscriminator for consolidation/cluster invalidations.- MCP_
NOTIFICATION_ DATA_ DOCUMENTS_ UPDATED datadiscriminator for document-level invalidations. Includes chunks (coalesced — chunks are a leaf of a parent document, so refetching the document covers both).- MCP_
NOTIFICATION_ DATA_ GRAPH_ UPDATED datadiscriminator for graph (triples) invalidations.- MCP_
NOTIFICATION_ DATA_ MEMORIES_ UPDATED datadiscriminator for episode-level invalidations. Coarse signal: “any episode-level memory changed; refetch what you care about”. MapsInvalidateEvent.kind = "episode".- MCP_
NOTIFICATION_ DATA_ MEMORY_ UPDATED - Fallback
dataforInvalidateEvent.kindvalues P4 doesn’t recognise. Defensive: a future writer command emits a new kind string we haven’t mapped yet → the client still sees a refetch signal rather than the bridge dropping the event silently. - MCP_
NOTIFICATION_ DATA_ TENANT_ UPDATED datadiscriminator for tenant-lifecycle invalidations (today fired by the GDPRforget_usercascade persolo_core::InvalidateEvent’s kind taxonomy).- MCP_
NOTIFICATION_ MESSAGE_ LEVEL levelfield of every MCPnotifications/messageenvelope this bridge emits. The spec permitsdebug/info/warning/error; v0.11.0 emitsinfobecause the invariant onsolo_core::InvalidateEventis that it ONLY fires after a successful writer-actor commit — there is no error/warning path.- MCP_
NOTIFICATION_ MESSAGE_ LOGGER loggerfield of every MCPnotifications/messageenvelope this bridge emits. The MCP spec requiresloggerto be a server-chosen identifier —"solo"distinguishes our messages from any proxy / middleware that might emit on the same stream.- MCP_
NOTIFICATION_ MESSAGE_ METHOD - JSON-RPC method literal for MCP
notifications/message. Held aspub constso audits can grep the wire literal once and trust it never drifts (Lesson #25 / #39 grep-ability).
Functions§
- map_
invalidate_ to_ message - Map one
solo_core::InvalidateEventinto the MCP spec’snotifications/messageJSON envelope. - spawn_
invalidate_ bridge - Spawn the per-session invalidate-bridge task.