7The in-process hub carrier: a direct call, and a published event beside it.
11Plan 259 P5, from the factory-decomposition design note ยง4. The hub has
12three carriers, and this is the first one tried: when both parties are
13in one process -- the orchestrator and an agent it is driving -- the
14message goes straight to the recipient's callback rather than out to a
15file and back. That is the direct path the operator asked for.
17Two things keep it honest.
19**It is chosen by PRESENCE, not by preference.** A party is present here
20only while something is attached for its address, so a session that has
21gone away simply falls through to the file lane and the message waits on
22disk. Nothing is dropped because a listener vanished.
24**Direct does not mean unlogged.** The hub records before it routes, so
25the log's entry is identical whether this carrier or the file lane moved
26the message -- design-note invariant 1, and a witness measures it by
27comparing two logs rather than reading it off this file. On top of that,
28when an event bus is injected the carrier publishes ONE
29``TurnEvent(kind="hub_message")`` per delivery, so a session's own event
30writer records the fleet traffic that crossed it for free.
32That published event carries ROUTING ONLY -- no body, no provenance, no
33answer. The hub's durable log is the record of what was said; this event
34is the record that something was said. A telemetry event anything on the
35bus may read must not become a second place a ruling's authority can be
40- :class:`BusCarrier` -- ``attach`` / ``detach`` / ``present`` /
41 ``deliver``, the :class:`~oc_chat.core.hub.Carrier` contract plus the
42 two attachment methods :meth:`~oc_chat.core.hub.Hub.attach` delegates
44- :class:`NoAttachmentError` -- the carrier's named refusal.
50 L2 - lifecycle events (an attach, a delivery)
51 L3 - semantic details (a refusal, a publish)
52 L4 - deep tracing (module load)
56- **The callback runs BEFORE the event is published.** Delivery is the
57 job; telemetry is the by-product, and a bus fault must never eat a
58 message that a recipient has already had.
59- ``present`` is false for ``operator@remote`` even when something is
60 attached for it. An in-process callback cannot be a phone, and saying
61 "present" would swallow a message the operator is waiting for. The
62 attach is still ALLOWED, so a control centre may watch that traffic;
63 it simply never wins the routing decision.
64- One address holds ONE callback. Re-attaching replaces; two would
65 deliver the same message twice.
66- The event is built by ``core/events.make_hub_message``, the one
67 factory in ``EVENT_FACTORIES``, so this module cannot drift from the
68 kind's declared payload shape.
69- The bus is DUCK-TYPED: anything with ``publish``. That keeps the
70 hub's closure free of the runtime's event stack while still letting a
71 live session pass its real :class:`~oc_chat.core.event_bus.EventBus`.
75- oc_diagnostics (structured debug logging via _dbg)
76- oc_orchestrator.hub (the contract; moved there by plan 272, and named
77 directly rather than through the shim because this module is the
78 consumer-side carrier that STAYED, not a caller that always spelled
80- oc_chat.core.events (the one hub_message factory)
83from __future__
import annotations
85from typing
import Any, Callable
87from oc_diagnostics
import _dbg
90from oc_orchestrator.hub
import (
91 ADDRESS_OPERATOR_REMOTE,
98_dbg(
"OC_CHAT.MAIN",
"module",
"hub_bus loaded", 4)
104DEFAULT_BUS_SESSION_ID =
"hub"
110DEFAULT_BUS_PARENT_EVENT_ID =
"hub-root"
113HubCallback = Callable[[HubMessage],
None]
117 """No in-process reader is attached for that address."""
121 """Deliver to an attached callback, and publish one event beside it."""
129 bus: Any |
None =
None,
130 session_id: str = DEFAULT_BUS_SESSION_ID,
131 parent_event_id: str = DEFAULT_BUS_PARENT_EVENT_ID,
133 func =
"BusCarrier.__init__"
138 _dbg(
"OC_CHAT.MAIN", func,
139 f
"bus={'injected' if bus is not None else 'none'}", 2)
143 def attach(self, address: str, callback: HubCallback) ->
None:
144 """Register *callback* as the in-process reader for *address*.
146 Replaces any existing one: two callbacks for one address would
147 deliver the same message twice, which no reader expects.
150 AddressError: VALIDATION_ERROR (L3) -- from the grammar.
152 func =
"BusCarrier.attach"
153 parse_address(address)
155 _dbg(
"OC_CHAT.MAIN", func, f
"attached {address}", 2)
158 """Remove *address*'s reader.
161 NoAttachmentError: VALIDATION_ERROR (L3) -- nothing was
162 attached. Silence here would hide a mistyped address for
163 the life of a session.
165 func =
"BusCarrier.detach"
166 parse_address(address)
169 f
"nothing is attached for {address}; there is no in-process "
173 _dbg(
"OC_CHAT.MAIN", func, f
"detached {address}", 2)
178 """True when something is attached AND the address is reachable here.
180 ``operator@remote`` is never reachable in-process, whatever is
181 attached for it: the address MEANS the operator is away from the
182 control centre, and the connector is the only carrier that can
185 if address == ADDRESS_OPERATOR_REMOTE:
189 def deliver(self, message: HubMessage) -> DeliveryReceipt:
190 """Call the recipient's reader, then publish one event.
193 NoAttachmentError: VALIDATION_ERROR (L3) -- nothing is
194 attached. The hub only reaches this after ``present``
195 said yes, so hitting it means the reader went away
196 between the two calls; refusing lets the log's entry
197 stand and the file lane redeliver.
199 func =
"BusCarrier.deliver"
200 callback = self.
_callbacks.get(message.recipient)
203 f
"nothing is attached for {message.recipient}; the bus carrier "
204 f
"cannot deliver {message.message_id}"
207 _dbg(
"OC_CHAT.MAIN", func,
208 f
"delivered {message.message_id} to {message.recipient}", 2)
210 return DeliveryReceipt(
211 carrier=self.
name, message_id=message.message_id,
212 detail=f
"attached callback for {message.recipient}",
218 """Publish one ``hub_message`` event, if a bus was injected.
220 Never raises into the delivery path: the recipient already has
221 the message, and a telemetry failure must not be reported as a
222 delivery failure. The bus itself already isolates subscriber
223 faults; this guards the construction and the publish call.
225 func =
"BusCarrier._publish"
229 self.
bus.publish(make_hub_message(
230 event_id=new_event_id(),
234 message_id=message.message_id,
235 hub_kind=message.kind,
236 sender=message.sender,
237 recipient=message.recipient,
238 subject=message.subject,
239 in_reply_to=message.in_reply_to,
240 summary=message.summary,
242 except Exception
as exc:
243 _dbg(
"OC_CHAT.MAIN", func,
245 f
"EXTERNAL_ERROR: could not publish hub_message for "
246 f
"{message.message_id} ({type(exc).__name__}: {exc}); the "
247 f
"message was delivered regardless", 2)
249 _dbg(
"OC_CHAT.MAIN", func, f
"published hub_message {message.message_id}", 3)
253 "DEFAULT_BUS_PARENT_EVENT_ID",
254 "DEFAULT_BUS_SESSION_ID",
None _publish(self, HubMessage message)
DeliveryReceipt deliver(self, HubMessage message)
None attach(self, str address, HubCallback callback)
bool present(self, str address)
None detach(self, str address)
None __init__(self, *, Any|None bus=None, str session_id=DEFAULT_BUS_SESSION_ID, str parent_event_id=DEFAULT_BUS_PARENT_EVENT_ID)