Orchestrata Ultimate › Plans › kernel-dispatch-reply-design
Kernel Dispatch Ack-Only + Reply Addressing — Design
Source: plans/kernel-dispatch-reply-design.md at 8a2cb42ad · last changed 2026-08-19 · first committed 2026-08-05 · 450 lines.
Prose pending.
Plan text
Kernel Dispatch Ack-Only + Reply Addressing — Design
Status: DESIGN ONLY (RUL-022 — no implementation in this artifact) Branch: feat/cxp-direct-transport · Date: 2026-08-05 · Author: analyst (design phase) Defects: (A) kernel violates its documented "send_packet is ack-only" contract — measured 13.9s/21.1s acks + silently dropped volk failures; (B) kernel reply stream has no per-consumer addressing — measured 50/50 reply loss across concurrent gateways.
Every as-built claim below was grounded via the AST toolkit
(scripts/serialize_codebase_ast.py {symbols,refs,source}), never by hand-reading
.py files. Specs are authoritative (RUL-013). Rulings consulted: RUL-002 (no
mocks), RUL-003 (env from profile, injected seams; tunables may default, infra
endpoints may not), RUL-011 (ports from env), RUL-020 (async contract), RUL-022
(design before build), RUL-023 (feature means wired), RUL-024 (pre-existing is
not a disposition).
Coordination: the volk-comms epic (plans/volk-comms-heartbeat-design.md
P1–P8) is landing on this branch NOW. This design touches a DISJOINT surface
(kernel_handler / grpc_server / gateway server + cxp handler); the two overlap
only as merge-adjacency in kernel/pipeline.py and kernel/cli_main.py — see
§7 sequencing. Nothing here duplicates P1–P8; one optional follow-up composes
its DeliveryOutcome (§4.1 D8).
1. AS-BUILT INVENTORY
1.1 Defect A path — UI send → gateway → kernel → volk (all AST-verified)
| Component | Location | Facts |
|---|---|---|
Gateway cxp.send |
gateway/handlers/cxp.py:95-166 (CxpHandler.handle_send, @traces spec=014 reqs=[REQ-014-071] + spec=016) |
Builds the traffic packet, broadcasts it, then awaits _maybe_reply_as_concierge(...) before returning the RPC result — the ws RPC does not resolve until the kernel round completes. |
| Concierge dispatch | gateway/handlers/cxp.py:168-237 (_maybe_reply_as_concierge, @traces spec=032 story=US-032-G243 reqs=[REQ-032-053,REQ-032-054]) |
Docstring states the CONTRACT verbatim: "send_packet is ack-only; the volk's reply returns via the gateway's reply consumer". Awaits self._kernel_client.send_packet(CXPPacket(from_agent="HUMAN", to_agent="U:Cx", ...)). The returned receipt dict is DISCARDED — only raised exceptions reach the except that emits the G243 unavailable notice; a {"success": False, "error": ...} receipt produces nothing. |
| Kernel packet handler | agents/kernel_handler.py:207-246 (_KernelPacketHandler.__call__) |
Sequential awaits: _govern (63) → _bind_memory (144) → _run_intent (158-175) → _build_reply (177-205) → return. The receipt is only constructed after the volk's FULL LLM round. On any exception: log "kernel packet_handler failed" + return {"success": False, "error": str(exc)} — a receipt, never a packet. |
| Intent execution | agents/kernel_handler.py:158-175 (_run_intent) → kernel/pipeline.py:294-378 (PipelineRunner.submit_intent) / :390-446 (respond) |
asyncio.to_thread(self._runner.submit_intent, ...) — correct thread posture for the blocking engine, but awaited inline. submit_intent runs governance, events.emit_intent, KG sync, then self.postman.route_sync(packet) — the synchronous volk round. |
| Volk LLM round | agents/volks/concierge/volk.py:213-266 (_handle_intent) |
await self._call_llm(CONCIERGE_SYSTEM_PROMPT, ...) — the 13.9s/21.1s inline cost. A ProviderError here propagates (next row). |
| Failure propagation | postman/sync_router.py:222-239 (_dispatch_sync) + :241-258 (_run_async_handler) |
Both re-raise handler exceptions (pool.submit(asyncio.run, handler(packet)).result()); so ProviderError → submit_intent → to_thread → __call__'s except → failed receipt → discarded by the gateway (row 2). The intent dies with zero sender-visible signal. |
| Receipt plumbing | cxp/grpc_client.py:91-122 (GRPCClient.send_packet) |
Returns {"success": receipt.success, "message_id", "error", "latency_ms"}. GRPCClientConfig.timeout_seconds = 30 (grpc_client.py:50-58) is the only reason a 21.1s inline round doesn't also fail the transport call. (Minor observation: host: str = "localhost" literal default in the same dataclass — RUL-003-inverse tension, overridden at every production construction site; out of scope here.) |
| Reply enqueue (today) | cxp/grpc_server.py:158-205 (CXPServicer.send_packet) |
After the handler returns, if result.get("reply_packet"): self._packet_queue.put_nowait(...) — the ONLY reply-publication point, reachable only after the inline round. |
| In-repo precedent | orchestrator/pipeline_kernel.py:130-158 (PipelineKernelClient.send_packet, @traces spec=003 story=US-004 reqs=[REQ-003-US-004-101]) |
The in-process fallback client ALREADY does ack-then-background: self._spawn(thread_id, self._run_pipeline(packet, thread_id)); return True. The real kernel path is the outlier. |
1.2 Defect B path — kernel reply stream → gateway consumer(s)
| Component | Location | Facts |
|---|---|---|
| Reply queue | cxp/grpc_server.py:142-152 (CXPServicer.__init__) |
self._packet_queue: asyncio.Queue[dict] = asyncio.Queue() — one global, unkeyed, unbounded queue for every reply from this kernel. |
| Stream RPC | cxp/grpc_server.py:278-289 (receive_packet) |
await self._packet_queue.get() in a timeout loop. request.agent_id is never read — the proto field exists (cxp/proto/cxp.proto:93-97 CXPReceiveRequest{agent_id, timeout_seconds, max_messages}) and both clients transmit it, the servicer ignores it. First get() wins; every additional consumer is a lottery entrant. |
| Second competitor | cxp/grpc_server.py:291-301 (subscribe) |
Drains the SAME queue; a packet that fails _matches_subscription is consumed and discarded, not requeued — any live subscriber silently eats replies. |
| Gateway pump | gateway/server.py:109-128 (_pump_kernel_replies_once) → :792-832 (_consume_kernel_replies, task created at _init_kernel:576-578) |
client.receive_packets(agent_id="HUMAN") (default parameter, line 113) → broadcast each reply on cxp.traffic. EVERY live gateway (deployed app-container one at 10.93.53.13 + every test-spawned one) runs this loop against the same kernel → measured 50/50 win/loss; the loser's reply broadcasts to zero subscribers of a dead gateway. |
| gRPC client | cxp/grpc_client.py:146 (receive_packets) → :318-351 (_proto_to_packet) |
Decodes "metadata": dict(proto.metadata) (line 350) — the metadata map already round-trips on receive. |
| cxpt client | cxp/direct_cxp_adapter.py:415-437 (DirectGRPCClient.receive_packets) |
Same CXPReceiveRequest, decoded by the same _proto_to_packet codec. |
| cxpt server | cxp/direct_cxp_adapter.py:114-249 (DirectCXPServer, .servicer:158, _dispatch:206-249) |
Bridges every RPC to the same CXPServicer instance via run_coroutine_threadsafe onto one loop — a routing fix inside CXPServicer covers BOTH transports with zero cxpt work. |
| Metadata carriers | proto map<string,string> metadata = 14 (cxp/proto/cxp.proto:69); _packet_to_proto:266-316 (writes proto.metadata.update(packet.metadata)); _proto_to_dict:329-363 (reads, line 361); _dict_to_proto:365-410 (writes, lines 401-402); model CXPPacket.__init__ has `metadata: dict[str, Any] |
NoneANDreply_to: str |
| Reply construction | agents/kernel_handler.py:177-205 (_build_reply) |
Builds reply_packet with message_id/from/to/thread_id/subject/payload only — no metadata, so addressing currently has no carrier even though the pipes support it. |
| Reply source | kernel/pipeline.py:524-532 (get_last_human_message) |
Returns human_inbox[-1] — no thread filter. A failed run whose inbox still holds an OLDER conversation's reply re-emits that stale packet (tagged with the OLD thread id via reply.thread_id or thread_id). Becomes a genuine concurrency race once processing is backgrounded. |
| Interim mitigations (shipped, not fixes) | vitest fileParallelism:false; chat e2e tests target only the deployed gateway |
Reduce the number of concurrent competitors in CI; do nothing for real multi-gateway topologies. |
1.3 Boot/wiring seams (found via refs, not guessed)
build_kernel_packet_handler(agents/kernel_handler.py:250) has exactly two production use sites (perrefs):agents/runtime_main.py:81(inside_serve:59-96,server = create_cxp_server(config, packet_handler=build_kernel_packet_handler())) andkernel/cli_main.py:594(import at 578).CXPServer.__init__(cxp/grpc_server.py:417-427) storesself._servicerbut exposes no accessor;DirectCXPServer.servicer(direct_cxp_adapter.py:158) is the property precedent to mirror.- Gateway kernel-client construction:
gateway/server.py:512-625(_init_kernel) →create_cxp_client(config=GRPCClientConfig(host, port)), thenself._rpc.set_kernel_client(kernel_client)— the pass-through isgateway/rpc.py:214 RPCHandler.set_kernel_client→gateway/handlers/cxp.py:79 CxpHandler.set_kernel_client. A consumer-id setter mirrors this exact chain. - Reply-consumer config seam:
gateway/config.py:173(kernel_reply_timeout_s),:179(kernel_reply_backoff_base_s). - Deployment: kernel + gateway run as systemd services in the
orchestrata-testappcontainer (10.93.53.13) viadeploy/orchestrata-test/— they roll together (version-skew note in §4.1 D7).
1.4 Existing tests on these paths (regression floor)
tests/gateway/test_kernel_reply_consumer.py—test_pump_broadcasts_volk_reply_to_cxp_traffic,test_pump_raises_when_server_unreachable,test_pump_recovers_after_server_restart.tests/gateway/handlers/test_cxp_kernel_dispatch.py—test_cxp_chat_dispatches_to_kernel_with_correlated_thread.tests/cxp/test_grpc_kernel_endpoint.py— endpoint resolution +test_server_registers_both_services,test_get_status_returns_live_roster.tests/cxp/test_direct_transport.py,test_direct_cxp_adapter.py,test_direct_transport_liveness.py— cxpt transport floor.
2. SPEC CONTRACT REVIEW (specs are authoritative — RUL-013)
SPEC-001 US-002 "Message Routing and Dispatch" — specs/001-kernel/checklists/requirements.md:75-130
(NOTE: SPEC-001's checklist numbers this story's requirements REQ-002-NNN,
distinct from the postman checklist's REQ-002-USn-NNN series.)
- REQ-001-US-002-001 — Route message based on TO field in CXP packet.
- REQ-001-US-002-003 — Support unicast (single recipient) routing.
- REQ-001-US-002-008 — Handle routing to offline Volks.
- REQ-001-US-002-009 — Handle routing to unknown recipients.
- REQ-001-US-002-019 — Provide delivery acknowledgments.
- REQ-001-US-002-030 — Handle queue overflow gracefully.
- REQ-001-US-002-031..035 — Track message status: queued / dispatched / delivered / acknowledged / failed.
- REQ-001-US-002-037 — Store status transitions with timestamps.
- REQ-001-US-002-038 — Notify sender of status changes.
SPEC-002 (postman) — specs/002-postman/checklists/requirements.md
- REQ-002-US-001-011 (line 35) — Support for NOTIFY message type.
- REQ-002-US-001-026 (line 54) — Notify sender of permanent failures.
SPEC-014 (gateway) — specs/014-websocket-gateway/checklists/requirements.md
- REQ-014-001/002 — Client subscribes to CXP traffic; server pushes CXP packets as they flow.
- REQ-014-071 — Client sends CXP packet via
cxp.send(the RPC that currently blocks).
SPEC-032 G243 — specs/032-concierge-volk/spec.md:380,422 + checklists/requirements.md:85-89
- REQ-032-053 — gateway MUST dispatch to the real live ConciergeVolk when reachable and MUST NOT fabricate a reply.
- REQ-032-054 — when not reachable or dispatch fails, gateway MUST return an explicit, machine-detectable "assistant unavailable" response — never a conversational-looking fallback.
The G243 distinction this design must honor: G243 forbids fabricated
conversational replies (a canned text pretending to be the assistant). An honest,
machine-generated FAILURE status packet — sent from the KERNEL's own identity,
flagged with a machine-detectable status field, never conversational prose posing
as U:Cx — is the opposite of a fabricated reply: it is exactly the
REQ-001-US-002-035/038 + REQ-002-US-001-026 sender notification, in the same
assistant_status convention _build_unavailable_reply (gateway/handlers/cxp.py:239-270)
already established (_ASSISTANT_STATUS_UNAVAILABLE = "unavailable", flag both
top-level and in payload, text plainly labelled an automated notice).
Status vocabulary already exists — cxp/_packet_enums.py:45 DeliveryStatus
(state machine QUEUED → DELIVERED|FAILED; DELIVERED → ACKNOWLEDGED|FAILED) and
PacketType.ERROR (_packet_enums.py:20-30). No new enums are needed.
3. GAP LIST (as-built vs spec, each with evidence)
| # | Gap | Evidence | Spec anchor |
|---|---|---|---|
| G1 | Ack is not ack-only: the full volk LLM round runs inline inside the send RPC. __call__:207-246 awaits _run_intent:158-175 (→ submit_intent:294-378 → route_sync → concierge _call_llm) before constructing the receipt; gateway handle_send:95-166 awaits the whole chain inside the ws RPC. Measured: cxp.send acked at 13.9s and 21.1s → every UI send blows the ws bridge's 3s RPC budget → false "Failed to send" rendered before the genuine reply lands. Contradicts the in-code contract at cxp.py:168-237 and the PipelineKernelClient precedent (pipeline_kernel.py:130-158). |
§1.1 rows 1-5, 9 | REQ-001-US-002-019, REQ-001-US-002-031/032, REQ-014-071 |
| G2 | Volk failure is silently dropped — no failure packet, and the failed receipt is discarded. ProviderError propagates cleanly to __call__ (re-raise chain verified at sync_router.py:222-258) → {"success": False} receipt → _maybe_reply_as_concierge never inspects the receipt (only raised exceptions hit its except) → no unavailable notice, no failure packet, sender learns nothing. Observed live (concierge volk.py:236 ProviderError). |
§1.1 rows 2, 6, 7 | REQ-001-US-002-035, REQ-001-US-002-038, REQ-002-US-001-026, REQ-032-054 |
| G3 | One unkeyed reply queue; receive_packet ignores agent_id. CXPServicer.__init__:152 single asyncio.Queue; receive_packet:278-289 never reads request.agent_id; every gateway's _consume_kernel_replies:792 competes; measured 50/50 reply loss across paired identical chat probes; loser's reply broadcast to zero subscribers. |
§1.2 rows 1-4 | REQ-001-US-002-001, REQ-001-US-002-003, REQ-014-002 |
| G4 | subscribe is a second silent competitor that DESTROYS non-matching replies. subscribe:291-301 get()s from the same queue and drops packets failing _matches_subscription without requeue. |
§1.2 row 3 | REQ-001-US-002-001, REQ-014-001 |
| G5 | Reply packets carry no addressing even though every codec supports it. _build_reply:177-205 omits metadata; proto field 14 + all four codec directions verified metadata-capable; model has metadata (and an unmapped reply_to) at _packet_impl.py:109. |
§1.2 rows 5-8 | REQ-001-US-002-001, REQ-001-US-002-039 |
| G6 | Stale-reply race in reply selection. get_last_human_message:524-532 returns human_inbox[-1] unfiltered; _build_reply stamps reply.thread_id or thread_id — an older conversation's reply can be re-emitted; under Defect A's background processing this becomes a live concurrency race between in-flight intents. |
§1.2 row 9 | REQ-001-US-002-033, REQ-001-US-002-039 |
| G7 (minor) | Bounded-overflow behavior is undefined for the reply queue (today unbounded, forever-growing for a dead consumer once queues are keyed). | §1.2 row 1 | REQ-001-US-002-030 |
| G8 (observation) | GRPCClientConfig.host = "localhost" literal default (grpc_client.py:55) — RUL-003-inverse tension; every production site overrides it; recorded for a follow-up sweep, not in these packets. |
§1.1 row 7 | RUL-003 |
4. CONTRACT DESIGN
4.1 Key decisions
Defect A — ack-immediately + background intent + honest terminal failure (G1, G2, G6):
- D1 — Ack-only
__call__, background the round (closes G1). For chat subjects (intent/response),_KernelPacketHandler.__call__runs ONLY the synchronous pre-flight —_govern(governance still sees every intent before ack; a denial returnssuccess=Truewithreply_subject="intent_denied"— CORRECTED 2026-08-05 during P-A3: the original text here saidsuccess=False, but the as-built contract, pinned bytest_kernel_handler_governance.py::test_govern_denies_forbidden_intent, returns a SUCCESSFUL receipt whose reply is the denial) — then mints/keeps the thread id and schedulesasyncio.create_task(self._process_in_background(...)), returning the ack immediately: the existing key shape (success/thread_id/reply/reply_payload/ reply_subject/reply_packet— reply fields empty) plusaccepted: True. This is the receipt = QUEUED/DISPATCHED acknowledgment (REQ-001-US-002-019/031/032).subject == "status"stays fully synchronous (_status_reply:119). Precedent:PipelineKernelClient.send_packet:130-158(_spawn+ return) — this design makes the out-of-process path match the in-process one. Threading posture (free-threaded 3.14t):create_tasklands on the servicer's own loop — grpc.aio's loop for the grpc transport, theDirectCXPServerloop for cxpt (_dispatch:206-249executes servicer coroutines there viarun_coroutine_threadsafe) — and the blocking engine call stays onasyncio.to_threadexactly as_run_intent:158-175does today. The handler holds strong refs (_bg_tasks: set, discard-on-done); tasks die with the server loop onstop()(bounded lifecycle, no daemon threads). - D2 — Reply sink seam (makes D1 possible). The background job cannot
return a
reply_packetthrough the receipt, so the handler gainsset_reply_sink(sink: Callable[[dict], None])and publishes replies itself. The sink isCXPServicer.enqueue_reply— a NEW public method that is also Defect B's single routing point.CXPServergains aservicerproperty (mirror ofDirectCXPServer.servicer:158;self._serviceralready exists atgrpc_server.py:426). Boot order at both construction sites:handler = build_kernel_packet_handler(); server = create_cxp_server(config, packet_handler=handler); handler.set_reply_sink(server.servicer.enqueue_reply). Back-compat:send_packet:158-205's existing inlineresult.get("reply_packet")branch delegates toenqueue_replytoo, so any handler still returning inline replies gets identical routing. - D3 — Honest terminal FAILURE packet (closes G2, respects G243). The
background job wraps the round; on exception (the verified ProviderError
propagation path) it sinks
_build_failure_packet(thread_id, error, reply_to):from_agent="KERNEL"(never the volk's alias — no impersonation),to_agent="HUMAN", samethread_id,subject="intent_failed",packet_type=PacketType.ERROR,delivery_status=DeliveryStatus.FAILED,timestamp(REQ-001-US-002-037), payload{"text": <plainly-labelled automated failure notice>, "error": str, "failure": true, "assistant_status": "failed"}— the same machine-detectable convention as_build_unavailable_reply:239-270, so the chat gadget detects it with the field it already checks. This is REQ-001-US-002-035 (track failed) + REQ-001-US-002-038/REQ-002-US-001-026 (notify sender); it is NOT a fabricated reply under G243 because it never claims to be the assistant and is machine-flagged end-to-end. Terminal contract: one terminal packet per accepted intent — the genuine reply (DELIVERED) or the failure packet (FAILED). The pre-existing "ran fine but produced no human-directed reply" case (produced=False, queued-for-offline path_route_sync_deliver:85-106) intentionally keeps today's no-packet semantics (REQ-001-US-002-018 queueing is Postman's job, not a failure). - D4 — Gateway checks the receipt it already has (closes G2's sync half).
_maybe_reply_as_conciergeinspects the send receipt:success=False(transport/handler-level failure — NOT governance denial, which returnssuccess=True+reply_subject="intent_denied"; corrected 2026-08-05, see D1) → emit the existing_build_unavailable_replynotice, same as a raised exception. One conditional; zero new machinery; REQ-032-054's "or dispatch fails" clause becomes true for the receipt path. - D5 — Thread-correct reply selection (closes G6).
PipelineRunner.get_last_human_messagegains an optionalthread_id: str | None = None; when given, scanhuman_inboxin reverse for the first packet with thatthread_id._build_replypasses the thread. Unfiltered call keeps byte-identical behavior (back-compat with every existing caller).
Defect B — reply addressing (G3, G4, G5, G7):
- D6 — Chosen: reply-to metadata stamped at send + per-consumer queues,
keyed drain via the EXISTING
agent_idfield. Three candidates were evaluated:Option Verdict Rationale (a) consumer-id in receive_packetsaloneREJECTED as sole mechanism The servicer cannot know which consumer owns a reply unless the reply is tagged; a key on the drain side without a tag on the supply side routes nothing. It IS the right drain half, and CXPReceiveRequest.agent_id(proto:93-97, transmitted by both clients, ignored today) already carries it — no proto change.(b) thread→consumer registry in Redis REJECTED Adds an infra hop + registration race (reply can beat the registration write) + TTL/cleanup lifecycle for dead gateways + Redis access from inside both transport servers. The sender already KNOWS the answer at send time; a registry re-derives it expensively. (c) reply-to metadata stamped at send CHOSEN (combined with (a)'s keyed drain) Zero proto changes (metadata map field 14 verified round-trip in all four codec directions, §1.2); zero new infra; one routing point ( enqueue_reply) covers grpc AND cxpt because both share the oneCXPServicer(§1.2 row 7); Defect A's failure packets inherit correct addressing for free.Mechanics: each gateway mints _reply_consumer_id = f"HUMAN@{uuid4().hex[:12]}"at construction; stamps metadata={"reply_to": <id>}on every kernel-boundchat packet; consumes with receive_packets(agent_id=<id>). The kernelhandler propagates inbound metadata["reply_to"]onto the reply AND failurepacket. ReplyRouter._pick_key:metadata.reply_toif present, elseto_agent(legacy unstamped traffic keeps queueing under"HUMAN"—backward compatible with every existing test/client), else a default key. - D7 —
ReplyRouter(new,cxp/reply_router.py), composed intoCXPServicer(closes G3/G4/G7). Per-keyasyncio.Queues created on demand;route(packet) -> strpicks the key and enqueues with bounded-overflow drop-oldest + traced warning (max_queuector param, default 256 — a tunable, not an infra endpoint, so a literal default is permitted under RUL-003's inverse rule).receive_packetdrainsqueue_for(request.agent_id);subscribedrainsqueue_for(request.agent_id)too (itsCXPSubscription.agent_idproto field exists, proto:99-103), which ends G4's cross-consumer destruction. All servicer coroutines execute on one loop per server (grpc.aio / DirectCXPServer), so a plain dict of queues needs no additional locking; the composedTracerrecords route/overflow decisions. Dead-consumer cost is bounded atmax_queuedicts per departed id (TTL eviction: explicit non-goal, below). Version-skew caveat: a stamping gateway against an old kernel (no propagation) would wait on an empty private queue. Kernel + gateway deploy together in the app container (deploy/orchestrata-test/), and Group B packets land kernel-side propagation (P-B2) before the gateway flip (P-B3), so the skew window does not occur in this fleet. - D8 — Coordination with the comms epic (compose, don't duplicate).
Nothing in P1–P8 touches these files' defect sites; conversely this design
does NOT add delivery-outcome types (P3 owns
DeliveryOutcome/aroute_outcome), does not touch heartbeats, and does not modifystatus_report. One optional follow-up (NOT in these packets): once P3 lands, aroute_sync_outcometwin would let the background job distinguish NO_ROUTE (REQ-001-US-002-009) from HANDLER_ERROR without exception inference; the measured defect (exception drop) does not need it, so Group A ships independent of P3's landing time.
Non-goals (MVP discipline): consumer-queue TTL eviction; ACKNOWLEDGED
lifecycle wiring (the acknowledge RPC exists at grpc_server.py:303 but no
production consumer acks — REQ-001-US-002-034 stays with its current owner); UI/chat
gadget rendering of intent_failed; multi-kernel fan-in; changing
PipelineKernelClient (already contract-compliant); mapping the model's unused
reply_to field into the proto (metadata is the carrier; a field-level mapping
is a SPEC-004 decision).
4.2 Contract diagram (also at plans/contracts/kernel-dispatch-reply.mmd)
Diagram source shown as text: Mermaid could not parse it (Error: Lexical error on line 22. Unrecognized text.).
classDiagram
direction LR
%% ═══ KERNEL DISPATCH ACK-ONLY + REPLY ADDRESSING — contract (analyst, 2026-08-05) ═══
%% REUSE (compose, never regenerate): cxp.grpc_server.CXPServicer (send_packet:158,
%% receive_packet:278, subscribe:291, codecs :329/:365 both metadata-capable),
%% CXPServer:414 (_servicer:426), DirectCXPServer:114 (shares the SAME CXPServicer via
%% .servicer:158 — one fix covers both transports), agents.kernel_handler
%% ._KernelPacketHandler:42, kernel.pipeline.PipelineRunner (submit_intent:294,
%% respond:390, get_last_human_message:524), gateway.handlers.cxp.CxpHandler
%% (_build_unavailable_reply:239 — machine-detectable precedent), gateway.server
%% .WebSocketGateway (_consume_kernel_replies:792), orchestrator.pipeline_kernel
%% .PipelineKernelClient.send_packet:130 (ack-then-background precedent),
%% cxp._packet_enums.DeliveryStatus + PacketType.ERROR, core.tracing.Tracer.
%% New module homes:
%% cxp/reply_router.py -> ReplyRouter (new)
%% edits: cxp/grpc_server.py, agents/kernel_handler.py, kernel/pipeline.py,
%% agents/runtime_main.py, kernel/cli_main.py, gateway/handlers/cxp.py,
%% gateway/rpc.py, gateway/server.py
class ReplyRouter {
<<class>>
+__init__(tracer: Tracer, max_queue: int) None
+route(packet: dict) str
+queue_for(consumer_id: str) Queue
-_pick_key(packet: dict) str
-_queues: dict
-_max_queue: int
}
%% traces spec=001 story=US-002 reqs=REQ-001-US-002-001,REQ-001-US-002-003,REQ-001-US-002-008,REQ-001-US-002-030
ReplyRouter o-- Tracer : composes (DI)
class CXPServicer {
<<existing — MODIFIED cxp/grpc_server.py>>
+__init__(packet_handler: Callable, reply_router: ReplyRouter | None) None
+enqueue_reply(packet: dict) None
+receive_packet(request: CXPReceiveRequest, context: Any) AsyncIterator
+subscribe(request: CXPSubscription, context: Any) AsyncIterator
}
%% traces spec=001 story=US-002 reqs=REQ-001-US-002-019,REQ-001-US-002-033,REQ-001-US-002-001
CXPServicer o-- ReplyRouter : composes (DI; keyed queues replace the single _packet_queue)
class CXPServer {
<<existing — MODIFIED cxp/grpc_server.py>>
+servicer() CXPServicer
}
%% traces spec=001 story=US-002 reqs=REQ-001-US-002-019
CXPServer o-- CXPServicer : composes (already; property mirrors DirectCXPServer.servicer:158)
class KernelPacketHandler {
<<existing — MODIFIED agents/kernel_handler.py>>
+__call__(packet: dict) dict
+set_reply_sink(sink: Callable) None
-_process_in_background(subject: str, thread_id: str, text: str, reply_to: str) None
-_build_failure_packet(thread_id: str, error: str, reply_to: str) dict
-_reply_sink: Callable | None
-_bg_tasks: set
}
%% traces spec=001 story=US-002 reqs=REQ-001-US-002-019,REQ-001-US-002-031,REQ-001-US-002-032,REQ-001-US-002-033,REQ-001-US-002-035,REQ-001-US-002-038
KernelPacketHandler ..> CXPServicer : reply_sink = servicer.enqueue_reply (wired at boot)
KernelPacketHandler ..> KernelPipelineRunner : submit_intent / respond via to_thread (background)
class KernelPipelineRunner {
<<existing — MODIFIED kernel/pipeline.py>>
+get_last_human_message(thread_id: str | None) CXPPacket | None
}
%% traces spec=001 story=US-002 reqs=REQ-001-US-002-033
class CxpHandler {
<<existing — MODIFIED gateway/handlers/cxp.py>>
+set_reply_consumer_id(consumer_id: str) None
-_maybe_reply_as_concierge(target: str, subject: str, thread_id: str, payload: dict) None
-_reply_consumer_id: str
}
%% traces spec=032 story=US-032-G243 reqs=REQ-032-053,REQ-032-054
CxpHandler ..> KernelPacketHandler : send_packet stamps metadata reply_to + receipt.success now CHECKED
class WebSocketGateway {
<<existing — MODIFIED gateway/server.py>>
-_reply_consumer_id: str
-_consume_kernel_replies() None
}
%% traces spec=014 story=US-001 reqs=REQ-014-001,REQ-014-002
WebSocketGateway ..> CXPServicer : receive_packets(agent_id=_reply_consumer_id)
WebSocketGateway o-- CxpHandler : composes (via RPCHandler.set_kernel_client:214 precedent)
patternRole map for gen-skeleton: ReplyRouter=strategy (routing policy,
fully generatable shell; _pick_key+overflow=leaf) · enqueue_reply=leaf ·
CXPServer.servicer=boilerplate property · _process_in_background/
_build_failure_packet=leaf (builder-written) · everything else = edits to
existing classes (builder/wirer tasks, not generated).
4.3 Wiring plan (RUL-023 / Hard Rule 8 — named production seams, found via refs)
| New element | Production importer / seam |
|---|---|
ReplyRouter |
cxp/grpc_server.py CXPServicer.__init__:142-152 (constructed default when not injected; replaces _packet_queue). Reaches cxpt automatically — DirectCXPServer wraps the same servicer (direct_cxp_adapter.py:158). |
CXPServicer.enqueue_reply |
cxp/grpc_server.py send_packet:179-180 (inline reply_packet branch delegates); agents/kernel_handler.py background job (via set_reply_sink). |
CXPServer.servicer property |
cxp/grpc_server.py:414-427 (exposes existing self._servicer:426). |
set_reply_sink wiring |
BOTH build_kernel_packet_handler use sites (per refs): agents/runtime_main.py:81 (_serve:59-96) and kernel/cli_main.py:594 — handler constructed first, then handler.set_reply_sink(server.servicer.enqueue_reply). |
| Failure packet + reply_to propagation | agents/kernel_handler.py _build_reply:177-205 (+ metadata passthrough) and _process_in_background (new). |
get_last_human_message(thread_id) |
kernel/pipeline.py:524-532; caller agents/kernel_handler.py:186. |
| Receipt check + reply_to stamp | gateway/handlers/cxp.py _maybe_reply_as_concierge:213-224 (stamp metadata, inspect receipt) — CXPPacket model accepts metadata= today (_packet_impl.py:109), and _packet_to_proto:312-313 already transmits it. |
set_reply_consumer_id |
Mirror of the existing chain gateway/server.py _init_kernel:574 → gateway/rpc.py:214 set_kernel_client → gateway/handlers/cxp.py:79 — same three files, same pass-through shape. |
| Gateway consumer identity | gateway/server.py WebSocketGateway.__init__:340 mints _reply_consumer_id; _consume_kernel_replies:792-832 passes agent_id=self._reply_consumer_id into _pump_kernel_replies_once:109 (parameter already exists — today it silently defaults to "HUMAN"). |
Transport constraint respected: metadata + CXPReceiveRequest.agent_id are the
only carriers, both already present in the proto (cxp.proto:69, 93-97) and
serialized by both transports — zero proto changes, zero new RPCs; the test
profile's CXP_TRANSPORT=cxpt path is covered by the shared-servicer fact.
5. TDD TEST PLAN (tests-first; real infra only — Redis 10.93.53.11, graph 10.93.53.10, kernel 10.93.53.13:50053, env profile test; no mocks/monkeypatch per RUL-002; slow/failing volks are REAL registered plugin-volk handlers via _PluginVolkAdapter (kernel/pipeline.py:50-94, SPEC-015 production feature) — no LLM in the loop, no test doubles)
| Test module | Test | Requirement | Behavior proven |
|---|---|---|---|
tests/cxp/test_reply_router.py |
test_route_by_reply_to_metadata |
REQ-001-US-002-001, REQ-001-US-002-003 | Packet with metadata.reply_to="HUMAN@abc" lands ONLY in that consumer's queue. |
test_unstamped_packet_routes_by_to_agent |
REQ-001-US-002-001 | No metadata → queued under to_agent ("HUMAN") — legacy compatibility. |
|
test_overflow_drops_oldest_and_traces |
REQ-001-US-002-030, REQ-001-US-002-008 | max_queue=3, 5 packets → oldest 2 gone, newest 3 present, overflow traced (dead-consumer boundedness). |
|
tests/cxp/test_receive_packets_per_consumer.py |
test_two_consumers_each_receive_only_their_own |
REQ-014-002, REQ-001-US-002-003 | Real CXPServer + two real clients: interleaved stamped replies; each receive_packets(agent_id=<own id>) drains exactly its own set — kills the measured 50/50 loss. Parametrized over both transports (grpc + cxpt via cxp_transport factory). |
test_legacy_human_consumer_still_served |
REQ-001-US-002-001 | Unstamped reply + receive_packets(agent_id="HUMAN") → delivered (back-compat floor). |
|
test_subscribe_no_longer_steals_other_consumers_replies |
REQ-014-001 | A live subscribe(agent_id=A) while consumer B receives → B's replies all arrive (kills G4). |
|
tests/agents/test_kernel_handler_ack_only.py |
test_ack_returns_before_slow_volk_completes |
REQ-001-US-002-019, REQ-001-US-002-031, REQ-001-US-002-032 | Runner with a REAL plugin volk whose handler sleeps 5s; send_packet ack in <1s, accepted=True, thread id present (kills the 13.9s/21.1s ack). |
test_reply_streams_after_ack_with_same_thread |
REQ-001-US-002-033 | After ack, receive_packets (within its timeout window) yields the genuine reply carrying the SAME thread_id. |
|
test_failing_volk_emits_failure_packet |
REQ-001-US-002-035, REQ-001-US-002-038, REQ-002-US-001-026 | Plugin volk handler that raises → a subject="intent_failed" packet on the stream: from_agent="KERNEL", matching thread_id, delivery_status="failed", error text present. |
|
test_failure_packet_is_machine_detectable_not_conversational |
REQ-032-053, REQ-032-054 | Failure packet asserts payload.assistant_status=="failed", payload.failure is True, from_agent != "U:Cx", packet_type=="error" — provably not a fabricated assistant reply. |
|
test_governance_denial_stays_synchronous |
REQ-001-US-002-019 | Denied action → receipt success=False immediately, NO background task spawned, no packet on the stream (governance sees the intent pre-ack). |
|
test_status_subject_stays_synchronous |
REQ-001-US-002-019 | subject="status" → full roster in the receipt, byte-compatible with today. |
|
test_reply_to_propagated_to_reply_and_failure |
REQ-001-US-002-001, REQ-001-US-002-039 | Inbound metadata.reply_to reappears on both the genuine reply and the failure packet. |
|
tests/kernel/test_get_last_human_message_thread_filter.py |
test_filter_returns_only_matching_thread |
REQ-001-US-002-033 | Inbox holding replies for threads T1,T2 → get_last_human_message(thread_id="T1") returns T1's, not T2's (kills G6). |
test_unfiltered_call_backcompat |
REQ-001-US-002-033 | No-arg call returns human_inbox[-1] exactly as today. |
|
tests/gateway/test_cxp_send_receipt_check.py |
test_failed_receipt_triggers_unavailable_notice |
REQ-032-054 | Kernel client whose receipt is success=False (real servicer, denied subject) → cxp.traffic carries the assistant_status="unavailable" notice (kills G2's sync half). |
tests/gateway/test_reply_consumer_addressing.py |
test_two_gateways_each_get_their_own_reply |
REQ-014-002, REQ-001-US-002-038 | Two gateway consumer loops (distinct minted ids) against one live kernel fixture; paired sends; each broadcast_fn observes exactly its own thread's reply — the production-shaped proof of the 50/50 fix. |
| regression (unchanged) | tests/gateway/test_kernel_reply_consumer.py (3), tests/gateway/handlers/test_cxp_kernel_dispatch.py (1), tests/cxp/test_grpc_kernel_endpoint.py, tests/cxp/test_direct_transport*.py |
— | Must stay green with zero edits. NOTE: test_cxp_chat_dispatches_to_kernel_with_correlated_thread relies on receive_packets' timeout window, which still covers the now-asynchronous reply. |
Runner discipline: scripts/run_tests.py --affected / targeted modules only,
live-observed (observe_pytest attach, RUL-009), 5-min cap per run; kernel
required (never --skip kernel).
6. BUILD PACKETS (each <5 min for a builder agent; tests-first inside each; Group A ships without Group B)
Group A — ack-only + honest failure (G1, G2, G6)
| # | Packet | Files | Depends on |
|---|---|---|---|
| P-A1 | get_last_human_message(thread_id=None) filter + tests/kernel/test_get_last_human_message_thread_filter.py |
kernel/pipeline.py:524-532 |
— |
| P-A2 | CXPServicer.enqueue_reply (initially wrapping the existing single queue) + CXPServer.servicer property + inline-branch delegation + enqueue tests |
cxp/grpc_server.py (152, 179-180, 414-427) |
— |
| P-A3 | Handler backgrounding: set_reply_sink / _process_in_background / _build_failure_packet / ack shape / _bg_tasks + tests/agents/test_kernel_handler_ack_only.py (ack/reply/failure/governance/status tests) |
agents/kernel_handler.py (158-246), uses P-A1's filter |
P-A1, P-A2 |
| P-A4 | Wiring: reply_sink at BOTH boot sites + gateway receipt check + tests/gateway/test_cxp_send_receipt_check.py + regression run of §5's floor |
agents/runtime_main.py:81, kernel/cli_main.py:594, gateway/handlers/cxp.py:213-224 |
P-A3 |
Group B — reply addressing (G3, G4, G5, G7)
| # | Packet | Files | Depends on |
|---|---|---|---|
| P-B1 | ReplyRouter + servicer keyed drain (receive_packet/subscribe by request.agent_id, enqueue_reply routes) + tests/cxp/test_reply_router.py + tests/cxp/test_receive_packets_per_consumer.py |
cxp/reply_router.py (new), cxp/grpc_server.py (142-152, 278-301) |
P-A2 |
| P-B2 | Kernel-side reply_to propagation onto reply + failure packets + test_reply_to_propagated_to_reply_and_failure |
agents/kernel_handler.py (_build_reply:177-205, _build_failure_packet) |
P-A3, P-B1 |
| P-B3 | Gateway consumer identity: mint + stamp + consume + set_reply_consumer_id chain + tests/gateway/test_reply_consumer_addressing.py |
gateway/server.py (340, 574, 792-832), gateway/rpc.py (~214), gateway/handlers/cxp.py (79, 213-224) |
P-B2 |
Sequencing vs the comms epic: no shared defect sites, but kernel/pipeline.py
(P-A1 here vs comms-P7 status_report) and kernel/cli_main.py (P-A4 here vs
comms-P8 boot wiring) are merge-adjacent — land each file's packets serially,
either order. P-A2/P-B1 are gen-skeleton-friendly (property/leaf-method shells +
traced test stubs); the rest are builder/wirer edits to existing classes.
Deploy note (RUL-012): Group A and Group B each end with redeploying the app
container's kernel+gateway services (deploy/orchestrata-test/) and re-running
the paired chat probe that measured the defect — B's acceptance is 2/2 paired
probes delivered to their own gateways, A's is ack <1s + a failure packet
observed on a forced provider error.