Kernel Dispatch-Reply Hardening (F4 / F5 / F7)

Status: design RATIFIED (commit d47388ed, RUL-022 design-before-build). Group H-A — the F5 terminal-totality group — is SHIPPED: commit e949984a, pushed and deployed to the test-fleet app container; certifying gate11 rc=0, 30,790 / 0 / 0 / 0 at a 265 s test phase (new best). F4 and F7 remain designed-not-built.

Digest of plans/kernel-dispatch-reply-hardening.md (+ contract plans/contracts/kernel-dispatch-reply-hardening.mmd). Parent: plans/kernel-dispatch-reply-design.md — the 7-packet epic shipped in 9914d662 that closed 13.9–21.1 s inline ack latency and the 50/50 reply loss from the single unkeyed reply queue. The adversarial whole-diff review of that epic filed three finding groups; this addendum designs their fixes.

F5 — terminal totality (landed FIRST; F4 depends on it) — SHIPPED e949984a

Contract: every accepted intent sinks exactly one terminal packet — reply | intent_no_reply | intent_failed. This AMENDS the parent design's D3, which consciously allowed no-packet on produced=False; once the ack promises a background round, silence is indistinguishable from loss.

Three verified holes, each ending an ACKED round with the sender hearing nothing:

  1. produced=False — Prolog may_execute deny makes submit_intent return None; the background round returned without sinking anything.
  2. produced=True but no inbox match — _build_reply found nothing and skipped the sink (reachable via respond's unknown-thread early-out and its reply-to-escalation branch).
  3. Shutdown cancellation — nothing drained the handler's _bg_tasks; CancelledError (a BaseException) escaped the except Exception arm, so every kernel restart (the deploy-time NORM) killed in-flight rounds silently.

Design: a total terminal function in _process_in_background (new intent_no_reply terminal via _build_no_reply_packet, sharing a _build_terminal_packet core with _build_failure_packet; G243-honest — from_agent="KERNEL", machine-detectable assistant_status="no_reply", never conversational); handler.shutdown(grace) draining _bg_tasks (KERNEL_BG_DRAIN_GRACE_S, default 30 s, covers the measured 11.5–21.1 s round); an except CancelledError arm that sinks a "kernel shutting down" failure terminal then RE-RAISES; and drain-before-transport-stop wiring at both boot sites (agents/runtime_main.py, kernel/cli_main.py).

All three packets LANDED in e949984a: P-H4 (terminal extraction + intent_no_reply + always-sink; 28/28), P-H5 (shutdown(grace) drain), P-H6 (wiring — runtime_main._graceful_stop on SIGTERM/SIGINT and _CXPKernelComponent.stop in cli_main; drain before transport stop, proven by flip-tested ordering). Adversarial-review must-fixes folded in: defensive KERNEL_BG_DRAIN_GRACE_S parse, try/finally at both drain sites, bounded post-cancel wait (5 s abandon). Named follow-ups filed from that review: F-3 accept-during-drain window, F-4 uncancellable in-thread rounds (plus a reproduced drain-cleanup deadlock when a round swallows cancellation), F-6 assistant_status not surfaced top-level.

F4 — same-thread round safety (designed, not yet built)

Ack-only dispatch made same-thread concurrency the normal case; three shared structures were never upgraded for it:

Packets: P-H1 (refcount) → P-H2 (balance), P-H3 (consume, after P-H4), P-H9 (bound) — F5 has landed, so this group is now unblocked.

F7 — ReplyRouter hygiene (designed, not yet built)

ReplyRouter had no eviction at all: every gateway restart strands its HUMAN@<12hex> queue forever, and any client on the insecure fleet port mints a queue per novel agent_id (unauthenticated, uncapped key growth).

Design (pattern: idle-TTL cache eviction with drain leases; deliberately NO background sweeper — a sweeper task would recreate exactly the untended-lifecycle class F5 exists to fix):

Packets: P-H7 (router) → P-H8 (servicer lease integration, both transports).

Sequencing spine

F5 before F4 (P-H4 → P-H3: consume-on-match makes no-match rounds more frequent, so the no-reply terminal must exist first) and P-H1 before P-H2 (refcount before balancing, else the unbind defect just relocates). Groups H-A/H-B/H-C are otherwise independent and individually shippable; each group ends with an app-container redeploy + a live acceptance probe. The H-A redeploy (e949984a, done) was the first restart that drains in-flight rounds instead of killing them.

As-built: F4 + F7 (groups H-B, H-C) — 2026-08-05

F4 (P-H1/P-H2/P-H3), all landed:

F7 (P-H7), landed:

Rider fixes in the same landing: narrow PluginVolkRegistryProtocol (SPEC-015) replacing the semantically-wrong full-registry annotation on PipelineRunner.plugin_volk_registry (7 pyright errors → 0); cli_main status health URL env-resolved (was literal localhost:8000; health lives on GATEWAY_HTTP_PORT/8766); fleet session.open PermissionError fixed via writable SESSION_ROOT through LAST-listed EnvironmentFile overrides (/etc/orchestrata/*-overrides.env — measured: EnvironmentFile= beats Environment=, the old kernel-unit comment claimed the opposite and its CXP_GRPC_HOST=0.0.0.0 was dead).

Adversarial-review round (2026-08-05, folded in before landing): the consume path's unlocked snapshot was itself the crash site — deque iteration raises RuntimeError on concurrent append and deque.remove raises IndexError mid-__eq__-scan (reachable GIL-on; failed the claim-check open). Fixed with one _human_inbox_lock over scan+match+remove and all three append sites; the forced-ValueError test became a real-threads race proof (2 writers + 2 consumers, exactly-once claims). Router sweep now runs on the mint path too and bypasses its interval under cap pressure (rotating drain-side minting could otherwise flood between ticks). Memory-balance tests observe the registry mid-round (bite even where memory infra is down); _env_float/_env_int gained the environ= seam; the gateway unit's own dead Environment=GATEWAY_HOST fell to the same overrides-file mechanism. Lesson recorded: sweep by seed-vs-assignment counting, not string presence — a reference in one test masked five missing host locks.

Amendment ratified (design §11): F-3 accept-during-drain → D-H11 handler-seam _draining flag + synchronous KERNEL_DRAINING receipt; packet P-H10, next.

New investigation (P1): cxpt native SIGSEGV in Server_dealloc — third py3.14t crash signature, first inside our own extension; repro = batch-under-load; tied to the F-4 cooperative-cancel work.

Recorded decision forks (design §10)

  1. Eviction constants — ADOPTED as env-tunables with literal defaults (CXP_REPLY_QUEUE_TTL_S=900, sweep 60 s, max_consumers 1024; RUL-003 inverse: tunables may default) — revisable without a code change.
  2. Kernel gRPC consumer authentication — DEFERRED: consumer ids are 48-bit capabilities on a fleet-internal port; a token handshake is SPEC-004 territory. Revisit if the port ever leaves the fleet subnet.
  3. Distinct intent_blocked terminal for Prolog-denied intents — DEFERRED: needs a submit_intent outcome-signature change (SPEC-000 REQ-000-033..041); intent_no_reply satisfies the terminal contract today.

Acceptance run (2026-08-05) — H-B/H-C PASS + the env split-brain lesson

Repo pointers