Repository navigation
Description
Priority: P1
Area: Adapter host
Repository / target branch: wavect/semaprax / wavect/v090
Reviewed commit: 5428b0805fa3cf35859c843a53e30c3252ef378c
Review date: 6 October 2026
Defect
The stdout reader detects unsolicited or malformed adapter output and calls Shared::violate. That records Closed::Violation, wakes any pending receivers and settles the owned process. When the adapter has no pending request, however, nobody delivers that closure to AdapterHandle::closed_outcome or quarantine.
On the next invocation, ensure_running checks whether the cached process is still open. For every closed process, including Closed::Violation, it takes the same path: drop_proc(..., Closed::Host("restart")), then prepare/spawn/initialize a new generation. It does not classify or propagate the saved close reason before the new business request is dispatched. A protocol-violating adapter can therefore be silently restarted merely by emitting its bad frame while idle instead of while a waiter is registered.
This violates the existing contract that protocol violations terminate and quarantine the provider for the session. It also makes state() misleading in the interval: the host reports Active even after the reader killed that generation for protocol corruption.
Executed reproduction
A real Python adapter fixture completes initialization and one admitted decision.evaluate request successfully. Only after invoke has returned does the test release a file barrier that makes the adapter send an unsolicited JSON-RPC response with unknown ID 999999.
The test waits until the first adapter PID is gone, proving the stdout reader handled and physically settled the violation. Immediately before the second request, the handle still reports Active. The second request returns Completed on a different process PID, and the handle is never Quarantined.
Executed test: host::tests::review_transport::idle_protocol_violation_is_restarted_instead_of_quarantined. Production bytes are unchanged in the source copy; only test modules were appended. The negative behavior is asserted deliberately, so the test's pass is proof that the bug is present, not proof that the product is fixed.
Smallest useful change
Reconcile a closed process generation's exact Closed reason with manager state before allowing replacement or further dispatch. Closed::Violation must preserve the original diagnostic and quarantine without another adapter launch. Use the existing generation ownership checks so a stale close from an older process cannot mutate a newer generation's state.
Route ordinary Exited/transport failures through the existing once-per-generation failure accounting where applicable, and keep intentional host shutdown/idle retirement distinct. Do not reset the breaker or relax quarantine merely to make restart work. Avoid a new general supervisor; this is a missing reconciliation between the existing process close state and the existing manager state machine.
Acceptance criteria
- Unknown-ID, malformed-JSON and adapter-initiated messages emitted with zero pending requests quarantine the current handle.
- The next invocation returns the original terminal protocol diagnostic with zero additional adapter starts and zero additional
harness/invokeframes. - Violation during an active request and the same violation between requests have equivalent quarantine behavior.
- A normal explicitly reaped idle adapter remains restartable, and an ordinary crash obeys the documented breaker policy.
- Reconciliation and concurrent invocation/reaping cannot account one generation twice or let an obsolete generation overwrite a newer state.
- A real fixture uses a barrier after the first call settled; a test that sends the malformed frame before the first reply is consumed would miss this gap.
Existing issue boundary
#502 fixed counting one failed generation once per waiter; its generation guard should remain. #548 validates the response envelope. Those mechanisms work when the violation reaches an active waiter. This issue covers the unobserved between-call close event and the next invocation's unconditional restart. #500 explicitly stated that closed-handle replacement must not silently restart a quarantined adapter; this patch should preserve that distinction.
Verification handoff
Read AGENTS.md, docs/HARNESS-HOST-V1.md and the lifecycle ownership sections. Add the regression to the existing host::tests module family; no new compiler-linking top-level integration binary is needed. Record exact starting/fixing commits, commands, nonzero selected/executed tests and relevant quality gates.
Activity
harness: bounded bridge backpressure, idle quarantine, strict chunkin…
wsdt commentedon Oct 6, 2026
Fixed in b5e3f37 (pushed to main). Root cause: ensure_running treated every closed generation alike and respawned, discarding a recorded Violation. reconcile_violation now runs under the core lock and quarantines the handle; state() reports Quarantined; the next invoke returns the original diagnostic with no new start and no harness/invoke frame; idle crashes are charged once per generation; host-initiated closes still restart. Evidence: host::tests::idle_violation_tests (unknown-id and malformed frames after the first reply, zero extra starts/invoke frames, idle-reap positive control); harness suite green.