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)

1.4 Existing tests on these paths (regression 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.)

SPEC-002 (postman) — specs/002-postman/checklists/requirements.md

SPEC-014 (gateway) — specs/014-websocket-gateway/checklists/requirements.md

SPEC-032 G243 — specs/032-concierge-volk/spec.md:380,422 + checklists/requirements.md:85-89

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):

  1. 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 returns success=True with reply_subject="intent_denied" — CORRECTED 2026-08-05 during P-A3: the original text here said success=False, but the as-built contract, pinned by test_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 schedules asyncio.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) plus accepted: 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_task lands on the servicer's own loop — grpc.aio's loop for the grpc transport, the DirectCXPServer loop for cxpt (_dispatch:206-249 executes servicer coroutines there via run_coroutine_threadsafe) — and the blocking engine call stays on asyncio.to_thread exactly as _run_intent:158-175 does today. The handler holds strong refs (_bg_tasks: set, discard-on-done); tasks die with the server loop on stop() (bounded lifecycle, no daemon threads).
  2. D2 — Reply sink seam (makes D1 possible). The background job cannot return a reply_packet through the receipt, so the handler gains set_reply_sink(sink: Callable[[dict], None]) and publishes replies itself. The sink is CXPServicer.enqueue_reply — a NEW public method that is also Defect B's single routing point. CXPServer gains a servicer property (mirror of DirectCXPServer.servicer:158; self._servicer already exists at grpc_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 inline result.get("reply_packet") branch delegates to enqueue_reply too, so any handler still returning inline replies gets identical routing.
  3. 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", same thread_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).
  4. D4 — Gateway checks the receipt it already has (closes G2's sync half). _maybe_reply_as_concierge inspects the send receipt: success=False (transport/handler-level failure — NOT governance denial, which returns success=True + reply_subject="intent_denied"; corrected 2026-08-05, see D1) → emit the existing _build_unavailable_reply notice, same as a raised exception. One conditional; zero new machinery; REQ-032-054's "or dispatch fails" clause becomes true for the receipt path.
  5. D5 — Thread-correct reply selection (closes G6). PipelineRunner.get_last_human_message gains an optional thread_id: str | None = None; when given, scan human_inbox in reverse for the first packet with that thread_id. _build_reply passes the thread. Unfiltered call keeps byte-identical behavior (back-compat with every existing caller).

Defect B — reply addressing (G3, G4, G5, G7):

  1. D6 — Chosen: reply-to metadata stamped at send + per-consumer queues, keyed drain via the EXISTING agent_id field. Three candidates were evaluated:
    Option Verdict Rationale
    (a) consumer-id in receive_packets alone REJECTED 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 one CXPServicer (§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-bound
    chat packet; consumes with receive_packets(agent_id=<id>). The kernel
    handler propagates inbound metadata["reply_to"] onto the reply AND failure
    packet. ReplyRouter._pick_key: metadata.reply_to if present, else
    to_agent (legacy unstamped traffic keeps queueing under "HUMAN" —
    backward compatible with every existing test/client), else a default key.
  2. D7 — ReplyRouter (new, cxp/reply_router.py), composed into CXPServicer (closes G3/G4/G7). Per-key asyncio.Queues created on demand; route(packet) -> str picks the key and enqueues with bounded-overflow drop-oldest + traced warning (max_queue ctor param, default 256 — a tunable, not an infra endpoint, so a literal default is permitted under RUL-003's inverse rule). receive_packet drains queue_for(request.agent_id); subscribe drains queue_for(request.agent_id) too (its CXPSubscription.agent_id proto 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 composed Tracer records route/overflow decisions. Dead-consumer cost is bounded at max_queue dicts 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.
  3. 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 modify status_report. One optional follow-up (NOT in these packets): once P3 lands, a route_sync_outcome twin 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.