Orchestrata Ultimate › Plans › kernel-dispatch-reply-hardening

Kernel Dispatch-Reply HARDENING — Design Addendum (F4 / F5 / F7+F8)

Source: plans/kernel-dispatch-reply-hardening.md at 8a2cb42ad · last changed 2026-08-19 · first committed 2026-08-05 · 539 lines.

Prose pending.

Plan text

Kernel Dispatch-Reply HARDENING — Design Addendum (F4 / F5 / F7+F8)

Status: DESIGN ONLY (RUL-022 — no implementation in this artifact) Parent: plans/kernel-dispatch-reply-design.md (7 packets, all shipped in commit 9914d662 on feat/cxp-direct-transport; app container redeployed on it) Findings: the SET-2 whole-diff reviewer's named follow-ups, filed in the session backlog (context.json deferred_work #393–395; report reports/claude-failure-report-2026-08-05.md "Late addendum 4"):

Every as-built claim below was grounded via the AST toolkit (scripts/serialize_codebase_ast.py {symbols,refs,attr-refs,source}), never by hand-reading .py files. Specs are authoritative (RUL-013). Rulings consulted: RUL-002 (no mocks/monkeypatch — injectable seams instead), RUL-003 (no infra-env literal defaults; tunables MAY default), RUL-009 (observed test runs), RUL-012 (done = wired + deployed), RUL-013, RUL-020 (async correctness), RUL-022 (this document), RUL-023 (feature means wired), RUL-024 (pre-existing is a diagnosis, not a disposition).


1. AS-BUILT INVENTORY (post-9914d662, all AST-verified)

1.1 Kernel handler (F4 memory bind, F5 terminal contract)

Component Location Facts
Ack-only dispatch agents/kernel_handler.py:382-453 (_KernelPacketHandler.__call__) For _CHAT_SUBJECTS = {"intent","response"} (line 57) with a sink set: govern → asyncio.create_task(self._process_in_background(...)) → strong ref into _bg_tasks (init at 68-86) → immediate accepted: True receipt. finally: unregister_memory(thread_id) at 453 (sync path only).
Background round agents/kernel_handler.py:290-336 (_process_in_background) _bind_memory → _run_intent → if not produced: return (no packet) → _build_reply → if reply_packet: sink(...) (empty dict → no packet). except Exception → sink(_build_failure_packet(...)). finally: unregister_memory(thread_id) at 336. asyncio.CancelledError is a BaseException — it ESCAPES the except Exception arm: a cancelled round sinks nothing.
Memory bind (handler side) agents/kernel_handler.py:213-225 (_bind_memory) register_memory(thread_id, self._mem) — binds the handler's shared engine.
Turn persistence agents/kernel_handler.py:171-181 (_remember) Uses self._mem DIRECTLY, never the registry — registry lifetime only matters DURING the volk round.
Reply build agents/kernel_handler.py:246-288 (_build_reply) self._runner.get_last_human_message(thread_id=thread_id) — peek, not consume. Stamps reply_to via the central _stamp_reply_to:99-114 (F2 fix, all four producers funnel through it).
_run_intent agents/kernel_handler.py:227-244 subject=="response" → respond(...) then unconditional produced=True — even when respond produced no inbox entry. submit_intent returning None → produced=False.
Failure packet agents/kernel_handler.py:338-380 (_build_failure_packet) from_agent="KERNEL", subject="intent_failed", PacketType.ERROR, DeliveryStatus.FAILED, machine-flags {"failure": true, "assistant_status": "failed"}.
Governance denial agents/kernel_handler.py:116-169 (_govern) Synchronous; returns success=True receipt with reply_subject="intent_denied" + stamped denial reply_packet — routed by send_packet's inline branch. NOT an F5 hole.

1.2 Pipeline runner (F4 consume/inbox, F5 produced=False)

Component Location Facts
Inbox kernel/pipeline.py:140 self.human_inbox: list[CXPPacket] = [] Unbounded. Writers (via attr-refs --attr human_inbox): submit_intent:380, respond:444, PipelineSessionMixin._human_handler (kernel/pipeline_session.py:401 — the postman "HUMAN" delivery handler, also the landing zone for terminal qa_complete/compliance_* packets nothing ever removes). Readers: get_last_human_message:548-551 (peek only). Annotation twin at pipeline_session.py:82.
Reply selection kernel/pipeline.py:534-554 (get_last_human_message(thread_id=None)) Thread-filtered reverse scan (P-A1). Returns without removing — the same packet can satisfy any number of waits.
Pipeline-side memory bind kernel/pipeline.py:279-302 (_bind_memory_engine) → called from submit_intent (:304-388) Registers a fresh MemoryEngine() under the SAME thread_id (overwrite) and never unregisters — today the handler's unconditional unregister_memory cleans it up as an accident of last-writer-wins.
Blocked intent kernel/pipeline.py:349-352 may_execute deny → submit_intent returns None → handler sees produced=False → no packet of any kind.
respond early-out kernel/pipeline.py:402-404 Unknown thread → prints [Error] Thread not found, returns; _run_intent still reports produced=True. Reply-to-escalation branch (:440-446) also appends nothing to human_inbox.
Memory registry memory/memory_context.py:96-101 (register_memory), :112-120 (unregister_memory), _providers: dict[str, Any] (module level, _lock-guarded) Last-writer-wins register; unconditional pop on unregister. Binders (per refs): kernel_handler (:224), kernel pipeline (:296, UNBALANCED), orchestrator pipeline_runner.py:756 (BALANCED — finally: unregister at :407-409, refcounting is a no-op there).

1.3 Reply router + servicer (F7)

Component Location Facts
Router cxp/reply_router.py:44-138 (ReplyRouter: __init__:57, _pick_key:76, queue_for:97, route:114) Exactly 5 symbols — no eviction method exists. _pick_key: metadata.reply_to > to_agent > DEFAULT_REPLY_KEY="HUMAN" (:42). route drop-oldest at max_queue=256 (per-queue bound, traced). Per-key asyncio.Queues created on demand, held forever. Single-loop threading contract documented in the class docstring.
Drain-side minting cxp/grpc_server.py:298-315 (receive_packet), :317-337 (subscribe) Both call self._reply_router.queue_for(request.agent_id) — any client on the insecure port mints a queue per novel agent_id, no auth, no cap on key count.
Router construction cxp/grpc_server.py:144-159 (CXPServicer.__init__) reply_router or ReplyRouter(Tracer.default()) — injectable; the default-construction seam is where env tunables belong.
Single publication point cxp/grpc_server.py:165-176 (enqueue_reply) + send_packet:199-200 inline delegation Confirmed: every reply (background sink AND inline receipt branch) funnels through enqueue_reply → route.
Consumer id mint gateway/server.py:382 f"HUMAN@{uuid.uuid4().hex[:12]}" — fresh per gateway construction; every restart/test-spawn strands the predecessor's queue. Fan-out chain: server.py:583 → rpc.py:230 → handlers/cxp.py:99 + handlers/orchestrator.py:82 (F1 fix). Stamp sites handlers/cxp.py:250-251, handlers/orchestrator.py:171-172.
Drain cadence gateway/server.py:109-128 (_pump_kernel_replies_once), :802-843 (_consume_kernel_replies), gateway/config.py:173 (kernel_reply_timeout_s, default window 5s) Windowed polling: a LIVE consumer's queue sees a queue_for touch at least every window — the fact that makes idle-TTL eviction safe.

1.4 Shutdown paths (F5)

Component Location Facts
Runtime boot site agents/runtime_main.py:59-98 (_serve) handler.set_reply_sink(server.servicer.enqueue_reply) (:84); SIGTERM/SIGINT → asyncio.ensure_future(server.stop()) (:86-88); run_serve_lifecycle (cxp/grpc_server.py:503-544) guarantees server.stop() in finally. Nothing anywhere cancels, awaits, or drains handler._bg_tasks — pending rounds die in asyncio.run teardown's cancel-all, and (§1.1) cancellation sinks no terminal packet.
Monolith boot site kernel/cli_main.py:642-662 (boot: handler :655, server :658, sink :662) + _CXPKernelServerLifecycle:323-332 (stop → server.stop(grace=1)) Same hole: the lifecycle object stops only the server.
Server stops cxp/grpc_server.py:488-492 (CXPServer.stop), cxp/direct_cxp_adapter.py:170-196 (DirectCXPServer.stop) Both stop transport only. DirectCXPServer.stop is the in-repo precedent for a deliberate, deadlock-aware drain discipline.

1.5 Regression floor (existing tests on these paths — must stay green)

tests/cxp/test_reply_router.py, tests/cxp/test_receive_packets_per_consumer.py, tests/agents/test_kernel_handler_ack_only.py (10), tests/kernel/test_get_last_human_message_thread_filter.py, tests/gateway/test_reply_consumer_addressing.py (3), tests/gateway/test_cxp_send_receipt_check.py (2), tests/gateway/test_kernel_reply_consumer.py (3), tests/gateway/handlers/test_cxp_kernel_dispatch.py, tests/agents/volks/test_memory_context.py, tests/cxp/test_direct_transport*.py.


2. SPEC ANCHORS (RUL-013; ids verbatim from checklists)


3. FINDING F4 — same-thread rounds (memory refcount, consume-on-match, bounded inbox)

3.1 Defect statement (three sub-defects, all unlocked by the epic's ack-only change)

Before 9914d662, one thread had at most one in-flight round (the send RPC blocked). Ack-only dispatch made same-thread concurrency the NORMAL case — a user sends a follow-up the moment the ack renders — and three shared structures were never upgraded for it:

F4a — memory bind is last-writer-wins + first-finisher-unbinds. _providers is a plain dict. Round R1: handler binds self._mem (kernel_handler.py:224); submit_intent then OVERWRITES with a fresh MemoryEngine() (pipeline.py:296) that nothing ever unregisters. Round R2 (same thread T, concurrent): binds again. Whichever round finishes FIRST runs unregister_memory(T) (kernel_handler.py:336) and pops the mapping while the other round's volk is mid-LLM — its recall() is best-effort and silently returns "". Failure scenario: M1 arrives (R1, measured 11.5–21.1s round). At t=3s the user sends M2 (R2, response on T). R2 completes at t=8s → unbind. R1's concierge recall at t=12s gets empty context → answers M1 with no memory of the conversation, silently — no error anywhere.

F4b — reply matching is peek-not-consume. get_last_human_message(thread_id=T) (pipeline.py:548-551) returns the newest matching packet WITHOUT removing it, and the respond path reports produced=True unconditionally (kernel_handler.py:234-236) even when it appended nothing (pipeline.py:402-404 unknown-thread early-out; :440-446 reply-to-escalation branch). Failure scenario: R1 emits reply P1 (inbox append at t=10, sunk at t=10). R2's round routes its outcome to a volk escalation — no inbox append — yet produced=True; R2's _build_reply re-matches P1 and re-emits it as the terminal answer to M2: a stale duplicate presented as a fresh reply. The symmetric race (R1 slow to build, R2's P2 lands first → both builds match P2, P2 emitted twice, P1 never) is reachable on the same structures.

F4c (absorbs F8) — human_inbox is unbounded. Three writers (pipeline.py:380, :444, pipeline_session.py:401 — the last also receives terminal qa_complete/compliance_* packets), zero consumers. A long-lived kernel process (the app container runs for weeks) accretes every human-directed packet forever.

FT-correctness hazard named en passant (RUL-024): appends run on asyncio.to_thread/sync-router worker threads while _build_reply's reversed() scan runs on the event loop — mutation during iteration raises RuntimeError; on 3.14t this is a live race today. The fix below removes it (snapshot-scan) rather than stepping over it.

3.2 Chosen design

3.3 Alternatives considered

Option Verdict
Per-round correlation ids (in-reply-to message_id threaded through postman) REJECTED for this packet set: a SPEC-002/SPEC-004 wire-contract change across route_sync; consume-on-match + thread filter delivers the filed guarantee ("earlier rounds' replies can't satisfy later waits") with a 1-keyword change. Correlation ids remain the eventual answer IF per-round (not per-thread) attribution is ever required — filed as a spec follow-up, not designed here.
Per-thread asyncio.Lock serializing background rounds REJECTED: changes user-visible concurrency semantics (a slow round delays the next round's execution), does nothing for the memory-bind defect (the pipeline binder still overwrites), and holds tasks alive longer at shutdown (worsens F5).
Refcount inside _KernelPacketHandler (count its own bg tasks per thread) REJECTED: the handler is not the only binder — pipeline._bind_memory_engine and the orchestrator path also register; correctness requires the count at the registry.
Per-thread inbox dict replacing the shared list REJECTED: restructures PipelineRunner + PipelineSessionMixin (annotation contract at pipeline_session.py:77-82) for no additional guarantee over filter+consume+bound.

4. FINDING F5 — zero-terminal-packet paths + background-task shutdown

4.1 Defect statement

The parent design's D3 terminal contract — "one terminal packet per accepted intent" — has three verified holes; each ends an ACKED (accepted: True) round with the sender hearing NOTHING, violating REQ-001-US-002-035/038 and REQ-002-US-001-026:

  1. produced=False — submit_intent returns None (Prolog may_execute deny, pipeline.py:349-352) → _process_in_background returns without sinking (kernel_handler.py:322). (The parent design consciously kept no-packet semantics here for the queued-for-offline case; the reviewer's F5 supersedes that call: once the ack promises a background round, silence is indistinguishable from loss. This addendum AMENDS D3.)
  2. produced=True, no match — _build_reply finds no inbox packet for the thread → reply_packet={} → if reply_packet: skips the sink (kernel_handler.py:326-328). Reachable today via respond's unknown-thread early-out (pipeline.py:402-404) and its reply-to-escalation branch (:440-446); D-H3 (consume-on-match) makes this path MORE frequent by design — F5's terminal must land before or with F4's consume packet (sequencing constraint, §7).
  3. Shutdown cancellation — nothing cancels/drains _bg_tasks (runtime_main.py:86-88 stops only the server; cli_main.py:332 likewise); pending rounds are cancelled by asyncio.run teardown, and CancelledError (a BaseException) escapes the except Exception arm (kernel_handler.py:331) — no failure packet, mid-round work silently lost, "Task was destroyed but it is pending" noise. Failure scenario: this is the DEPLOY-TIME NORM, not an edge — every deploy/orchestrata-test/ kernel restart (RUL-012's own acceptance step) cancels in-flight rounds; the UI thread that was acked waits forever across the restart.

4.2 Chosen design

4.3 Alternatives considered

Option Verdict
Distinct intent_blocked terminal for the Prolog-deny case DEFERRED (user-decision fork §9.3): distinguishing it requires submit_intent to return an outcome — a SPEC-000 US-004 (REQ-000-033..041) signature change. The generic intent_no_reply terminal satisfies the terminal contract now; both cases funnel through one code path.
Sink terminals from a task done-callback instead of in-coroutine REJECTED: a done callback cannot tell whether the coroutine already sank its terminal without extra shared state; the in-coroutine except CancelledError arm is the direct seam and keeps RUL-020's re-raise discipline visible.
asyncio.TaskGroup replacing _bg_tasks REJECTED: TaskGroup wants scoped lifetimes; __call__ is fire-and-forget by design. The shipped set + an explicit shutdown() is the minimal delta.
Reusing intent_failed with assistant_status="no_reply" REJECTED: a non-failure under a failure subject is dishonest signaling (RUL-017's spirit); the vocabulary is additive and cheap.

5. FINDING F7 — ReplyRouter eviction + unauthenticated queue minting

5.1 Defect statement

ReplyRouter (cxp/reply_router.py:44-138) has exactly five methods and no eviction anywhere; queue_for creates on demand and entries live forever. Three exposures:

  1. Rotating consumers (the normal fleet mode). Every gateway construction mints a fresh HUMAN@<12hex> (gateway/server.py:382); every gateway restart and every test-spawned gateway strands its predecessor's queue — bounded per-queue at 256 packets (drop-oldest) but unbounded in KEY COUNT on the long-lived app-container kernel.
  2. Unauthenticated drain-side minting. receive_packet:307 and subscribe:330 call queue_for(request.agent_id) for ANY caller on the insecure fleet port — one novel agent_id per call mints one dict entry, an arbitrary-rate growth vector requiring no credentials.
  3. Spoofable reply_to / drain-steal. Nothing authenticates a consumer id; receive_packet(agent_id=<victim>) drains the victim's queue. Mitigated in practice by unguessability (48-bit random suffix); the legacy "HUMAN" key is predictable BY DESIGN (back-compat). Failure scenario: weeks of suite runs against 10.93.53.13 leave thousands of dead keyed queues (monotonic RSS growth, growing dict); any buggy/hostile fleet client can accelerate it without bound.

5.2 Chosen design (pattern: idle-TTL cache eviction with drain leases; lazily swept — deliberately NO background task, applying F5's own lesson)

Sanctioned behavior change (explicit): "a reply routed before a consumer starts draining still arrives once it does" now holds only within idle_ttl_s; a consumer absent longer loses its queue and contents (traced). Everything else — precedence, per-queue drop-oldest 256, legacy "HUMAN" key — is unchanged (§8).

5.3 Alternatives considered

Option Verdict
Background sweeper task REJECTED: creates exactly the untended-task lifecycle class F5 exists to fix (another thing to drain at stop); the lazy sweep needs no shutdown story and costs O(keys) at most once per minute.
TTL on packets, not queues REJECTED: bounds content, not KEY growth — empty dict entries still accrete.
Evict on stream close (queue lifetime = connection lifetime) REJECTED: consumers are windowed pollers (one receive_packet call per 5s window, _pump_kernel_replies_once:109-128); a live gateway's queue would be destroyed between every window, breaking the connect-before-reply contract entirely.
LRU cap only (no TTL) REJECTED as sole policy: a below-cap population of dead queues never gets cleaned; TTL is the primary collector, the cap is the flood backstop.
Authenticated consumer registration RPC Genuine option, DEFERRED to user decision (proto addition + client changes on both transports; SPEC-004 territory).

6. CONTRACT DIAGRAM

plans/contracts/kernel-dispatch-reply-hardening.mmd — changed surfaces only, consistent with §§3–5. patternRole map for gen-skeleton: _QueueEntry=dataclass (fully generatable) · open_drain/close_drain/ _sweep=leaf (builder-written policy) · shutdown/_graceful_stop=leaf · _build_terminal_packet/_build_no_reply_packet=leaf · register_memory/unregister_memory refcount + get_last_human_message consume + submit_intent balance = edits to existing code (builder tasks, not generated).


7. BUILD PACKETS (each ≤ one ~5-min builder brief; tests-first inside each)

Dependency spine: P-H1 → P-H2 (refcount before balance — see D-H2) and P-H4 → P-H3 (no-reply terminal before consume-on-match — see §4.1.2). Groups are otherwise independent and individually shippable.

Group H-A — F5 terminal contract (lands first: F4 depends on it)

# Packet Files Tests (new/extended) Depends
P-H4 _build_terminal_packet extraction + _build_no_reply_packet + always-sink _process_in_background agents/kernel_handler.py (290-336, 338-380) tests/agents/test_kernel_handler_ack_only.py: test_no_match_round_emits_no_reply_terminal (respond to unknown thread → intent_no_reply, assistant_status="no_reply", failure is False, same thread_id, reply_to propagated — REQ-001-US-002-033/038); test_no_reply_terminal_is_machine_detectable (from_agent=="KERNEL", non-conversational — REQ-032-053/054); the produced=False deny case shares this exact sink call (same code path; a real-Prolog deny fixture may add it, never a mock). Regression: all 10 existing ack-only tests unchanged. —
P-H5 shutdown(grace) + except CancelledError → failure terminal + re-raise agents/kernel_handler.py same module: test_shutdown_drains_inflight_round (real slow _PluginVolkAdapter volk sleeping 5s, shutdown(grace=10) → genuine reply sunk, no cancel); test_shutdown_cancels_and_sinks_failure (grace=0.1 → intent_failed, error text "shutting down", delivery_status="failed" — REQ-001-US-002-035/038, REQ-002-US-001-026) P-H4
P-H6 Wiring: _graceful_stop helper + signal-handler swap; _CXPKernelServerLifecycle composes handler, stop() drains first agents/runtime_main.py (86-88, new helper), kernel/cli_main.py (323-332, 655-662) tests/agents/test_runtime_main.py: test_graceful_stop_drains_handler_before_server_stop (real server + slow round → terminal observed on the reply queue before stop returns); tests/kernel/test_cli_main_kernel_boot.py: lifecycle-stop twin P-H5

Group H-B — F4 same-thread rounds

# Packet Files Tests Depends
P-H1 Refcounted registry memory/memory_context.py (96-120, _providers shape) tests/agents/volks/test_memory_context.py: test_double_register_single_unregister_keeps_provider, test_balanced_unregister_drops_and_purges_caches, test_newest_engine_wins_while_counted (REQ-010-021) —
P-H2 Balance submit_intent's bind kernel/pipeline.py (submit_intent:304-388 try/finally) tests/kernel/ new test_pipeline_memory_bind_balance.py: test_registry_empty_after_submit_intent (real runner + registered plugin volk; module registry inspected — state read, not a double); test_concurrent_round_keeps_binding_until_last_unbind (two rounds, one thread — provider survives the first finisher) P-H1
P-H3 Consume-on-match kernel/pipeline.py (get_last_human_message:534-554 + snapshot-scan), agents/kernel_handler.py (_build_reply:269 passes consume=True) tests/kernel/test_get_last_human_message_thread_filter.py: test_consume_removes_matched_packet, test_peek_default_unchanged (REQ-000-039 floor); tests/agents/test_kernel_handler_ack_only.py: test_second_round_never_reemits_first_rounds_reply (two same-thread rounds, second produces nothing → its terminal is intent_no_reply, NOT a duplicate of round 1's reply — REQ-001-US-002-033/039) P-H4
P-H9 Bounded inbox kernel/pipeline.py:140 (deque + human_inbox_max ctor tunable), kernel/pipeline_session.py:82 (annotation) tests/kernel/ (same new module): test_inbox_bounded_drop_oldest (cap+1 appends → oldest gone, newest present — REQ-001-US-002-030); regression: _human_handler append + get_last_human_message forms on deque —

Group H-C — F7 router eviction

# Packet Files Tests Depends
P-H7 _QueueEntry + drain leases + lazy TTL sweep + cap (+ injectable clock) cxp/reply_router.py tests/cxp/test_reply_router.py: test_idle_queue_evicted_after_ttl (injected fake-forward clock — DI, no monkeypatch), test_active_drain_never_evicted, test_route_refreshes_activity, test_cap_evicts_oldest_idle_inactive_first, test_eviction_traced_with_dropped_count (REQ-001-US-002-008/030); floor: all existing router tests byte-unchanged —
P-H8 Servicer lease integration + env-tunable router construction cxp/grpc_server.py (144-159, 298-315, 317-337) tests/cxp/test_receive_packets_per_consumer.py: test_queue_survives_between_poll_windows_within_ttl, test_streaming_consumer_immune_to_eviction, test_drain_minted_queue_evicted_after_close_and_ttl (REQ-014-002); parametrized over both transports (grpc + cxpt — shared servicer). Floor: per-consumer + addressing suites unchanged P-H7

Runner discipline (RUL-009, 5-min cap): scripts/run_tests.py --affected / targeted modules only, live-observed (observe_pytest attach), kernel required — never --skip kernel (RUL-010). No mocks/monkeypatch anywhere (RUL-002): slow/failing volks are REAL registered _PluginVolkAdapter handlers (kernel/pipeline.py:50-94, SPEC-015 production feature); time is an injected clock; registries are inspected, never patched. Real infra per env/test.env: Redis 10.93.53.11, graph 10.93.53.10, kernel container 10.93.53.13.


8. COMPAT CONSTRAINTS (the 9914d662 reply contract keeps working)

Shipped contract Status under this design
metadata.reply_to > to_agent > DEFAULT_REPLY_KEY precedence (_pick_key:76-95) UNCHANGED — no edit to _pick_key.
HUMAN@<12hex> consumer ids (gateway/server.py:382) + set_reply_consumer_id chain (rpc:230 → cxp:99 / orchestrator:82) UNCHANGED — no gateway edits in any packet.
Per-queue drop-oldest 256 (route:114-138) UNCHANGED. The ONLY sanctioned lifetime change is D-H10 whole-queue idle eviction: never while a drain is open, never within idle_ttl_s of activity, always traced.
Legacy unstamped traffic under "HUMAN" UNCHANGED routing; the "HUMAN" entry is subject to the same TTL as any key (a legacy consumer idle > TTL re-mints on its next poll; packets routed during its absence beyond TTL are dropped-with-trace — the pre-eviction behavior for such a consumer was unbounded growth, not delivery).
Ack receipt shape (success/thread_id/reply/reply_payload/reply_subject/reply_packet + accepted) and _CHAT_SUBJECTS UNCHANGED.
Governance denial = success=True + reply_subject="intent_denied" (P-A3 corrected contract) UNCHANGED — _govern untouched.
get_last_human_message() no-arg / thread-filtered peek Byte-identical when consume is omitted.
Reply/failure packet shapes + _stamp_reply_to funnel (F2 fix) Preserved; _build_terminal_packet is an extraction, both wrappers keep funneling through _stamp_reply_to.
Subject vocabulary ADDITIVE only: intent_no_reply is new; intent_failed/intent_denied unchanged. UI chat-gadget rendering of the new assistant_status="no_reply" is a separate UI follow-up (non-blocking — unknown statuses pass through cxp.traffic broadcast untouched).
register_memory/unregister_memory signatures UNCHANGED (refcount is internal); orchestrator's balanced pair unaffected.

9. WIRING & DEPLOYMENT (RUL-012 / RUL-023)

Every new element's production importer, named:

Element Production seam
_build_no_reply_packet / total terminal agents/kernel_handler.py _process_in_background (already wired to the sink at both boot sites via set_reply_sink).
handler.shutdown agents/runtime_main.py:86-88 signal path (via _graceful_stop) AND kernel/cli_main.py _CXPKernelServerLifecycle.stop:332 — the only two build_kernel_packet_handler production sites (per refs, unchanged since the parent design §1.3).
Refcounted registry Already imported at agents/kernel_handler.py:36-37, kernel/pipeline.py:40, orchestrator/pipeline_runner.py:751 — internal change, wiring inherited.
submit_intent balance / consume / deque In-place edits inside kernel/pipeline.py — reached by every kernel boot.
Drain leases + eviction cxp/grpc_server.py receive_packet/subscribe — reaches BOTH transports automatically (DirectCXPServer wraps the same CXPServicer, direct_cxp_adapter.py:158). Env tunables resolved at CXPServicer.__init__:159.

Deployment: the test-fleet kernel + gateway run as systemd services in the orchestrata-test app container at 10.93.53.13 (deploy/orchestrata-test/) — restart = deploy; each group ends with a container redeploy. Note: the F5 drain (P-H6) changes what the restart itself does — the redeploy that ships Group H-A is the first restart that drains in-flight rounds instead of killing them.

Acceptance probes (post-redeploy, per group):


10. NON-GOALS & USER-DECISION FORKS

Non-goals (MVP discipline, unchanged from parent unless noted): ACKNOWLEDGED lifecycle wiring (REQ-001-US-002-034); UI rendering of intent_failed/ intent_no_reply; multi-kernel fan-in; per-round correlation ids (§3.3); proto-level reply_to field mapping (metadata stays the carrier).

Forks needing the user (recommendations attached):

  1. Eviction policy constants — idle-TTL 900s + max_consumers 1024 + sweep 60s (RECOMMENDED, §5.2 safety argument) vs a pure LRU cap vs connection-lifetime queues. All are ctor/env tunables, so the choice is revisable without a code change.
  2. Kernel gRPC consumer authentication — RECOMMEND deferring: ids are 48-bit capabilities on a fleet-internal insecure port; a token-handshake RPC is a SPEC-004 proto change. Revisit if the port ever leaves the fleet subnet.
  3. intent_blocked distinct terminal for Prolog-denied submit_intent — RECOMMEND deferring (needs a REQ-000-033..041 signature change); intent_no_reply covers the terminal contract today.

11. AMENDMENT (2026-08-05) — F-3 accept-during-drain window

11.1 Defect statement (reviewer finding F-3, filed at the F5 landing)

_KernelPacketHandler.shutdown(grace) snapshots self._bg_tasks and drains the snapshot — but the accept path (__call__) keeps spawning NEW rounds for packets arriving during the (default 30s) drain window. Post-snapshot rounds are never awaited by the drain, and their terminal packets sink into a transport that _graceful_stop/_CXPKernelComponent.stop closes the moment the drain returns. Residual of the F5 hole: terminal totality holds per round, but the terminal has no live consumer.

11.2 Chosen design — D-H11: handler-seam _draining flag (pattern: gate

at the single accept seam; transport-agnostic)

11.3 Build packet

Packet Content Files Tests Depends
P-H10 D-H11 _draining flag + synchronous KERNEL_DRAINING receipt agents/kernel_handler.py tests/agents/test_kernel_handler_ack_only.py: test_accept_during_drain_gets_draining_receipt (start a deliberately slow round; call shutdown() concurrently; hand the handler a fresh intent mid-drain; assert immediate KERNEL_DRAINING terminal, _bg_tasks gained no member, and the slow round's own terminal still lands), test_draining_receipt_preserves_thread_and_reply_to, test_non_spawning_subject_unaffected_while_draining; floor: every existing shutdown/drain test byte-unchanged F5 (shipped e949984a)

Executes AFTER the F4+F7 certify+ship cycle — not folded into it.