# Review 005 — Serving-Loop Concurrency and `op/register` Composition (post-remediation review of `f84d214`) ## Status Units 1–3 remediated and verified (G-01..G-05). All findings closed; the review is resolved. See Remediation log. ## Scope Post-remediation review of commit `f84d214` (review 004 Units 1–3: per-session fork registry, connect-side serving loop, `op/register`). The remediation landed all six F-findings' mechanisms; this pass reviews the landed implementation against the decided spec (ADR-022 amendment 2026-09-03, ADR-047 §4 amendment #2, review 004's acceptance gates) rather than re-litigating the findings. Every finding below was verified directly in source at tree `f84d214`. The headline finding (G-01) was verified **empirically**: a probe test was added to the tree, run, observed to reproduce the defect, and removed — the tree at this review's baseline carries no source changes. Findings continue alkcall review numbering with the new prefix `G` (001–004 used P/C/R/A/B/C/D/F — each review numbers independently). Cross-repo context: the alkhttp re-pointing claimed by the remediation log was verified in that repo (`5b62307` — `open-questions.md:102-112`, `decisions/048…md:19-25`). Unit 4 (alkhttp wiring) remains downstream and is not reviewed here. ## Baseline verification (this pass) ``` cargo test → 581 passed, 0 failed cargo test --all-features → 598 passed, 0 failed cargo clippy --all-targets -- -D warnings → clean cargo clippy --all-features --all-targets -- -D warnings → clean cargo fmt --check → clean cargo check --target wasm32-unknown-unknown → clean cargo clippy --target wasm32-unknown-unknown -- -D warnings → clean cargo doc --no-deps → clean ``` All gates the remediation log claims reproduce. The suite is green — and again the green is partial: the gates test the flows they name, and the one flow the ADR amendment promises most loudly (nested composition of peer-announced ops) is the one no gate exercises. See G-02 for why the existing gate cannot catch G-01. ## Verdict - **Unit 2's fork surface is solid.** Interior mutability, lock discipline, fork independence, validator carry, and the self-referential bootstrap-discovery closure are all correct and tested (see Non-findings). - **Unit 3's serving loop resolves the F-04 frame gap but inherits the dispatch loop's serial-inline shape** — and the F-05 flow it exists to enable is exactly the shape that deadlocks under it. The ADR-022 amendment's "invocable via nested composition" promise is unmechanized for wire-dispatched handlers (the primary caller shape); a 30s sweeper timeout masquerades as resolution. - **The F-05 acceptance gate does not exercise the forwarding stub** — the one component that is genuinely new protocol surface. It verifies announce-lands-in-overlay and the consumer serves, by calling the announced op *directly*, bypassing the stub entirely. - **`op/register`'s collision gate is overlay-only**, so a peer-announced op can shadow the serving side's own ops in nested composition (connections resolve before base in `PeerCompositeEnv`). The architecture decisions (fork as dispatch registry; overlay as landing zone; bootstrap-op set; opt-in serving) remain sound. The defects are in the serving loop's concurrency model and in what the gates measure. ## Severity legend Same scale as review 004: - **[critical]** — a decided spec invariant is violated in a way that makes a promised capability unreachable end-to-end; or corrupts data. - **[major]** — a core protocol path cannot serve a decided behavior; works only via shapes the spec does not describe. - **[minor]** — drift, doc/spec inconsistency, or a missing convenience with no correctness impact. --- # Part A — The serving loop's concurrency model ## G-01 [major] — Same-connection nested composition deadlocks the serving loop; the forwarding stub's nested call resolves only via the 30s sweeper **Status: REMEDIATED (Unit 1)** — see Remediation log; both gates added and verified load-bearing against the pre-fix loop. **ADR drift:** ADR-022 amendment (2026-09-03) §`op/register`: the announced op is *"invocable via nested composition (`env.invoke`)"*; the amendment's whole point is that the hub's handlers compose peer-announced ops. Review 004 F-05's recommendation said the same: the hub-side handler wraps announcements *"as a forwarding handler that issues a nested `call.requested` back over channel 0"*. **Verified:** YES, by code trace and by an empirical probe (see Verification log). Mechanism, in four steps: 1. `Dispatcher::serve_single_stream` awaits `dispatch()` **inline** in its read loop for `call.requested` frames (`src/protocol/dispatch.rs:1009-1014`) — the same serial-inline shape as the pre-existing accept-side `run_loop_single_stream` (`:765-770`). Only the Sink arm is spawned (`:1031-1053`); Once responses and Sub pumps (`pump_stream_single_stream`, awaited inline at `:1027-1029`) hold the loop. 2. The F-05 forwarding stub (`make_forwarding_handler`, `src/client/from_call.rs:392-415`) issues its nested call via `connection.call_with_payload` (`:408`) — over **the same connection** whose loop dispatched the parent. In single-stream mode the pending resolves only when *some* loop reads the `call.responded` frame (`connection.rs:274-303` — register pending, write frame, await receiver; resolution is the reader's job). 3. The only reader of that connection is the serving loop itself — which is blocked in step 1 awaiting the parent dispatch, whose handler is blocked in step 2 awaiting the nested call. Circular wait. The transport buffers the response frame; nobody reads it. 4. The 30s sweeper (`DEFAULT_CALL_TIMEOUT`, `connection.rs:32`; `evict_expired`, run every 10s per `SWEEPER_INTERVAL`, `dispatch.rs:44`) is the only thing that breaks the cycle — the nested call resolves as `CallError::timeout`. **Empirical probe (this pass):** hub serves `op/register` + a `hub/compose` handler that invokes the connection overlay's registration (the forwarding stub — the exact resolution `PeerCompositeEnv` → `OverlayOperationEnv` produces for nested composition) from inside a wire-dispatched handler; consumer announces `consumer/exec`, then calls `hub/compose` over the wire. Result: ``` PROBE: hub/compose resolved after 30.000761619s: Err(CallError { code: "TIMEOUT", message: "request timed out", retryable: true }) ``` A second run with an 8s timeout confirmed the hang is unconditional (not load-dependent): zero progress until the sweeper. The probe was removed after evidence capture; the tree is unchanged. **Consequences (verified):** - The ADR-022 amendment's flagship flow — a hub handler composing a peer-announced op — fails for every wire-dispatched parent handler. It "works" only for parents dispatched from *in-process* contexts (where the serving loop is idle and free to resolve the nested call's response) — a shape the ADR does not describe. Per the severity legend this is a decided behavior served only via an undescribed shape; it borders [critical] for the composition flow specifically. - The same circular wait applies to composition of `from_call` *imports* on the accept side: `run_loop_single_stream` has the same inline shape, and the connection overlay's imported-op stubs ride the same connection. That hazard is **latent pre-`f84d214`** (the accept side had the inline loop and `register_imported` before this commit) — `f84d214` makes it load-bearing by (a) adding the wire path that populates overlays with stubs, (b) putting serving loops on the connect side where both directions are live by design, and (c) writing the composition promise into ADR-022. - The same starvation is not limited to the stub: while the loop serves *any* Sub (`pump_stream_single_stream` inline), the side's own outbound pendings queue unresolved. A serving-enabled consumer that calls out while serving a long-lived Sub to the same peer eats 30s timeouts on its own calls. The F-04 gate tests the two directions only **sequentially** (hub→consumer resolves, *then* consumer→hub) — never interleaved. - Head-of-line blocking: the peer's subsequent requests queue behind each inline dispatch. **Fix (Unit 1):** spawn the Once dispatch and the Sub pump the way the Sink arm already is (the `SharedFrameWriter` already serializes concurrent frame writes, so per-request frame ordering is preserved); track spawned task handles for teardown on loop exit (the Sink arm's `in_flight_sinks` pattern, generalized). Keep read-loop resolution unblocked at all times. Apply the same rework to `run_loop_single_stream` (or factor the shared loop) — the accept side has the identical latent hazard via imported-op composition. Acceptance: the probe above (recreated as a permanent gate) resolves in < 1s; a concurrent-interleaving gate (outbound call resolving while a Sub is being served) passes without sweeper intervention. ## G-02 [major] — The F-05 acceptance gate bypasses the forwarding stub it claims to prove **Status: REMEDIATED (Unit 1)** — the stub-exercising gate and the interleaved-directions gate are added; see Remediation log. **Verified:** YES. `op_register_announce_then_hub_call_routes_back_to_consumer` (`src/channels/client.rs:1412-1543`) asserts the announce resolves and the overlay holds the op — then calls `accept_conn.call("consumer/exec", …)` (`:1529`). That is a **direct wire call**: the hub's dispatcher resolves `consumer/exec`… which is not on the hub at all — the frame crosses to the consumer, whose serving loop dispatches it from its *local* registry. The forwarding stub (`op_register_handler`'s `register_imported` bundle) is never invoked. The gate's comment — *"the forwarding stub routes the nested call back over channel 0"* — describes a path the test does not take. **Consequence:** the one behavior that is genuinely new protocol surface (stub routing) is untested, and the one defect it hides (G-01) is precisely in that path. This is the same failure class review 001 flagged ("all coverage is unit-level / the integration seams are where the real problems live") — the remediation added e2e gates, but the F-05 gate measures an adjacent, easier path. The F-04 gate is likewise sequential-only (see G-01's third consequence). **Fix (fold into Unit 1):** add a gate that exercises the stub — announce → a wire-dispatched hub handler composes the announced op via `ctx.env` (or directly via the overlay registration) → resolves from the consumer. This is the removed probe, productized; it is the acceptance test G-01's fix must pass. Optionally extend the F-04 gate with an interleaved-directions phase. --- # Part B — The `op/register` composition surface ## G-03 [major] — Announced ops can shadow the serving side's own ops in nested composition; the collision gate checks the overlay only **Status: REMEDIATED (Unit 2)** — see Remediation log; the ADR-022 amendment records the collision policy. **ADR drift:** ADR-018 (composition authority) and ADR-019 (the overlay is the *landing zone* for imported ops — a layer beneath the deployment's own registry, not a rival to it); ADR-022 amendment (silent on name collisions with the serving side's base registry). **Verified:** YES. `op_register_handler`'s collision gate is `connection.overlay_contains(&request.spec.name)` (`src/registry/op_register.rs:142`) — it consults **only the connection overlay**. The serving registry (the fork the session dispatches over) is not consulted. Meanwhile `PeerCompositeEnv` resolves **connections before base** (`src/registry/env.rs:230-245` — session, then `connection_order`, then base), and `compose_root_env` attaches the dispatching connection's overlay (`dispatch.rs:207-214`). Consequence: a peer announces an op named `fs/readFile` (or any name the serving side has registered — `services/list` itself is fair game). The registration lands in the overlay (forced `Internal`, `FromCall` — both per spec). From then on, every handler **wire-dispatched from that peer's connection** that nested-composes that name via `ctx.env` resolves to the peer's forwarding stub instead of the serving side's own op. The forced `Visibility::Internal` does not help here — it blocks wire invocation, not `env.invoke` (`OverlayOperationEnv` gates on `AccessControl` only, `connection.rs:749-836`; the composed child context is `internal: true` by design). Concretely: a hub handler processing peer A's request composes `fs/readFile` expecting *its own* registry op (with the deployment's `scoped_env`, capabilities, and ACL); it silently gets peer A's stub — peer A chooses where the call goes and what it returns. Composition authority (ADR-018) is decided by the composing handler's *deployer*; a same-name peer registration silently rewrites it. The effect is scoped to handlers whose root context is that connection's (`compose_root_env` attaches that connection's overlay), which bounds the blast radius — but that is exactly the flagship F-05 flow. **Fix (Unit 2):** decide the collision policy and record it in the ADR-022 amendment. Recommended: `op_register_handler` also rejects names registered on the serving registry (pass the fork — or a name-set closure — into the handler alongside the connection, the same way `install_bootstrap_discovery` closes over its registry); `ALREADY_EXISTS` for base collisions regardless of `replace` (the reconnect path needs to re-announce *peer* ops, not to overwrite the deployment's). The alternative — changing `PeerCompositeEnv` to resolve base before connections — would break `from_call`'s namespace-prefixed imports (prefixed names don't collide) is not needed for that, but would change long-decided layering semantics; the registration-side gate is the cheaper, narrower door. ## G-04 [minor] — `resource_id_path` does not survive the spec wire round-trip **Status: REMEDIATED (Unit 3)** — see Remediation log. **Verified:** YES. `spec_to_json_pub` serializes name/namespace/op_type/visibility/schemas/error_schemas/access_control (+ `channel_open`/`publish_schema` markers; `src/registry/discovery.rs:211-234`) — `resource_id_path` is not serialized. `rebuild_spec_for` cannot parse it (`src/client/from_call.rs:209-286`), and the field exists on `OperationSpec` (`src/registry/spec.rs:190`). An announced (or `from_call`-imported) op that declares ownership-scoped resource extraction silently loses it; the rebuilt spec's ACL checks run with `resource_id: None`. This is a pre-existing gap in the `from_call` import shape, not introduced by `f84d214` — but `op/register` makes it load-bearing for a second wire surface (peer-announced specs) that explicitly promises "the serializable parts" of a registration. Note `namespace` *is* serialized but `rebuild_spec_for` ignores it in favor of the `namespace_prefix` parameter — consistent for `op/register` (`replace`/re-announce uses `None`), worth a comment either way. **Fix (Unit 3):** add `resource_id_path` to both halves of the round-trip plus a round-trip test. Additive optional field in the `services/schema` JSON shape — no existing consumer breaks. ## G-05 [minor] — `install_bootstrap_discovery` registers `services/list-peers`; the ADR-022 amendment's bootstrap set doesn't name it **Status: REMEDIATED (Unit 3)** — see Remediation log. **Verified:** YES. `install_bootstrap_discovery` registers `services/list`, `services/list-peers`, and `services/schema` (`src/registry/discovery.rs:290-313`; `list-peers` at `:300`). The ADR-022 amendment's bootstrap set lists exactly three ops — `services/list`, `services/schema`, `op/register` (`022-…contract.md:380-393`). Review 004's remediation log says "list-peers" too; the ADR text was never updated. Consequence: none at runtime (installing a fourth op is strictly additive, and `services/list-peers` is what makes F-05's "peer-announced ops are discoverable" true). It is a doc/spec-consistency drift on a set the ADR declares *"closed."* **Fix (Unit 3):** add `services/list-peers` to the ADR-022 amendment's bootstrap-op list (recommended — `from_call`-import discovery already relies on it) or stop installing it; one line either way. --- # Non-findings (verified correct, recorded to bound the re-review) - **The fork surface (F-02/F-03) is solid.** Lock discipline is correct: `registration()` clones under a read guard and drops it before handler invocation (`registration.rs:190-192`), so self-referential discovery handlers cannot deadlock their own registry; `register` takes the two maps' write locks sequentially and never across an `await`. `fork()` deep-copies both maps (handlers are `Arc` closures — cheap, and the F-03 constraint that closures must carry is met); validators carry; fork independence is tested both directions; `OperationRegistryBuilder::from_registry` seeds the builder path. - **`install_bootstrap_discovery` (F-06) is correct** — handlers closed over the same `Arc` they're registered on, so post-install per-session registrations are visible at call time; ACL filtering stays per-caller; the fork gate (`fork_registry_open_op_resolves_and_is_discoverable`) exercises exactly the review-004 acceptance shape. - **`serve_single_stream`'s pending-resolution arms match the two half-loops they compose.** Frame by frame against `run_loop_single_stream` (`dispatch.rs:765-905`) and `dispatch_envelope` (`connection.rs:717-741`): responded/completed/ error resolve pendings; aborted tries both tables (in-flight sink aborts, then the pending cascade via `handle_abort`); published routes inbound sinks with the same validation/keep semantics; teardown (fail_all + sink drop + sweeper abort) matches. The direction disambiguation by table membership (documented at `dispatch.rs:940-966`) is correct as written — G-01 is *not* a frame-routing bug; the arms are right, the loop's scheduling is the defect. - **The pure-consumer default is unchanged.** `from_connection` delegates with `None` and keeps the resolution-only read pump; no behavior change for existing callers. - **`op/register`'s DTO and unit semantics are clean** — one spec serialization on the wire (`spec_to_json` shape), forced `Internal`/`FromCall` on landing (unit-tested), `replace` semantics tested, `ALREADY_EXISTS` collision gate tested (against the overlay — see G-03 for the gap). - **The alkhttp cross-repo claims check out.** `open-questions.md` OQ-05 and ADR-048's re-point are present in that repo at `5b62307`. - **No non-English text reached the tree.** The remediation session's language slip (reported anecdotally) did not land in any code, doc, or metadata: a CJK-range regex sweep over all `.rs`/`.md`/ `.toml`/`.json` in the repo returns zero matches at `f84d214`. - **Gates reproduce.** All commands in Baseline verification pass at the review tree, matching the remediation log's claims (581 default / 598 all-features). --- # Remediation plan Sequenced by dependency. All units are alkcall work; Unit 4 (alkhttp wiring) stays downstream and should **not** start before Unit 1 — alkhttp's serving consumers would compose over the same connection and hit G-01 immediately. (All three units landed — see Remediation log.) ## Unit 1 — Concurrent serving loop + a stub-exercising gate (G-01, G-02) - Rework `serve_single_stream`'s `EVENT_REQUESTED` arm: spawn the Once dispatch and the Sub pump (the Sink arm's existing pattern, generalized; `SharedFrameWriter` serializes frames; track handles for teardown). Apply the same rework to `run_loop_single_stream` or extract the shared loop. - Add the G-02 gate: announce → wire-dispatched hub handler composes the announced op via nested composition → resolves from the consumer. This is the removed probe, productized; it is the acceptance test for G-01. - Optional: an interleaved-directions phase on the F-04 gate (outbound call resolving while a Sub is being served). - Gate: the probe recipe (Verification log) resolves in < 1s; no sweeper evictions in the gates. ## Unit 2 — `op/register` collision policy (G-03) - `op_register_handler` gains a serving-registry name-set (closure or `Arc`); base-registry name collisions reject with `ALREADY_EXISTS` regardless of `replace`. - ADR-022 amendment: record the collision policy (peer-announced ops may collide with peer-announced ops — `replace` governs; never with the serving side's own registrations). - Gates: announcing a base-registered name (External *and* Internal) fails loudly; announcing a distinct name still lands; nested composition of a base op is unaffected by an unrelated announce. ## Unit 3 — Round-trip completeness + doc alignment (G-04, G-05) - `resource_id_path` through `spec_to_json_pub` + `rebuild_spec_for` + round-trip test. - ADR-022 bootstrap list gains `services/list-peers` (or drop it from the installer; recommend adding). --- # Remediation log ## Unit 3 — Round-trip completeness + doc alignment (G-04, G-05) — LANDED **G-04 fix.** `resource_id_path` now rides both halves of the spec wire round-trip: `spec_to_json_pub` serializes it as an optional `resource_id_path` string (`src/registry/discovery.rs`), and `rebuild_spec_for` parses it into the rebuilt spec's fourth constructor argument (`src/client/from_call.rs`). Additive optional field — absent stays absent, no existing consumer breaks (verified by the companion gate). `rebuild_spec_for`'s doc note from the review ("namespace *is* serialized but ignored in favor of `namespace_prefix`") was addressed by leaving the behavior as-is: for `op/register` the parameter is `None` (consistent) and for `from_call` the prefix is authoritative — the round-trip tests pin the `name` field handling. **Gates:** `spec_round_trips_resource_id_path` (serialize → parse → field intact) and `spec_without_resource_id_path_stays_absent_through_round_trip` (absent key serializes nothing; rebuilt `None`) in `src/client/from_call.rs`. **G-05 fix.** The ADR-022 amendment's bootstrap-op set gained `services/list-peers` with a dated note (review 005 G-05) explaining that the installer has registered it since the amendment landed and the doc lagged the code. The set remains closed at four. **Verification (post-fix):** ``` cargo test → 589 passed, 0 failed cargo test --all-features → 606 passed, 0 failed cargo clippy --all-targets -- -D warnings → clean cargo clippy --all-features --all-targets -- -D warnings → clean cargo fmt --check → clean cargo clippy --target wasm32-unknown-unknown -- -D warnings → clean cargo doc --no-deps → clean ``` --- ## Unit 2 — `op/register` collision policy (G-03) — LANDED **Fix shape.** `op_register_handler` now takes the serving registry alongside the connection (`op_register_handler(connection, serving_registry)` — the review's "name-set closure" recommendation, materialized as the `Arc` itself, mirroring how `install_bootstrap_discovery` closes over its registry). The handler checks `serving_registry.registration(name)` **before** the overlay gate and rejects base collisions with `ALREADY_EXISTS` regardless of `replace`; overlay collisions keep the existing `replace` semantics. Announced ops may collide with announced ops, never with the serving side's own registrations. **ADR-022 amendment:** the 2026-09-03 amendment's `op/register` section gained a "Collision policy (amended 2026-09-04, review 005 G-03)" paragraph recording the decided policy and the rationale (the `PeerCompositeEnv` connections-before-base resolution would let an unscreened announce rewrite composition resolution; visibility is irrelevant to the gate). The ADR's status line notes the sub-amendment. **Call sites updated:** both e2e gates pass the fork the session dispatches over (`Arc::clone(&accept_registry)` after `fork()`), which is the production shape — the collision set is exactly the registry the serving loop dispatches against. **Gates:** - `handler_rejects_base_registry_collision_even_with_replace` — base-registered `fs/readFile` (External) + `replace: true` → `ALREADY_EXISTS`; the overlay stays clean and the serving registration is untouched. - `handler_rejects_collision_with_internal_serving_op` — an `Internal` base op is equally protected (the review's point that forced `Visibility::Internal` on the *announced* spec never helped: `OverlayOperationEnv` gates on `AccessControl`, and the composed child is `internal: true` by design). - `handler_overlay_collision_still_governed_by_replace` — announce/ announce collisions still follow `replace` (the base gate is scoped to the serving registry, not widened into the overlay). - `nested_composition_of_base_op_unaffected_by_unrelated_announce` — after a successful distinct-name announce, composing the base op through the real `compose_root_env` shape (`PeerCompositeEnv` + `attach_peer(conn.overlay_env())`) resolves the serving side's own op, not a peer stub. (Unit-level, using the actual env types rather than a new full-wire fixture — the wire shape is already covered by the Unit-1 gates.) **Deliberate non-change:** `PeerCompositeEnv` resolution order is untouched (connections before base) — the review's recommendation; reordering would change long-decided layering semantics (ADR-019/ADR-024) for no gain the registration-side gate doesn't deliver. **Verification (post-fix):** ``` cargo test → 587 passed, 0 failed cargo test --all-features → 604 passed, 0 failed cargo clippy --all-targets -- -D warnings → clean cargo clippy --all-features --all-targets -- -D warnings → clean cargo fmt --check → clean cargo clippy --target wasm32-unknown-unknown -- -D warnings → clean cargo doc --no-deps → clean ``` --- ## Unit 1 — Concurrent serving loop + stub-exercising gates (G-01, G-02) — LANDED **Fix shape.** `dispatch()` was split into a synchronous start half and an awaited invocation: - `Dispatcher::dispatch_start` (`src/protocol/dispatch.rs`) runs the sync prefix (identity resolution, root context, op-type branch) and returns a `StartedDispatch`: `Once` (the invocation as a boxed future), `Stream` (the `ResponseStream`, returned synchronously — `invoke_streaming` is a sync call), or `Sink` (started **inline**, unchanged). `dispatch()` is now a thin wrapper (`Once` = await the boxed future) and keeps its shape for `dispatch_requested`/`handle_stream`/the gateway. - The Sink start stays inline **by design**: its `chunk_tx` must be in `in_flight_sinks` before the next `call.published` frame can be routed; the loop insert cannot race the feed. Pub handlers that block forever inside their sink future are a separate (pre-existing, unreported) shape — the read loop no longer waits on any handler except for the few instructions of the sink start itself. - Both single-stream loops' `EVENT_REQUESTED` arms spawn the Once invocation (`spawn_once_dispatch`), the Sub pump (`spawn_stream_pump`), and the sink's response writer (`spawn_sink_response_writer`); handles are tracked in a `spawned` list and aborted at loop exit (teardown: sink-map clear → spawn aborts → `fail_all` → sweeper abort). `in_flight_sinks` moved behind `Arc` because the ABORTED/PUBLISHED/ERROR arms must not hold the guard across the `chunk_tx.send().await` (non-`Send` guard across an await — the reason the pre-fix loop couldn't just be `tokio::spawn`ed piecemeal). Guard-dropping (`let entry = ...remove()` before the await) keeps lock discipline: no lock is held across an await anywhere in the loops. - **`run_loop_single_stream` (the accept side) got the pending-resolution arms** (`EVENT_RESPONDED`/`COMPLETED`/`ERROR` → the outbound pending map). Previously it served only — an accept-side nested-composing handler (e.g. a `from_call` imported-op stub riding the same connection) had no loop resolving its response frames at all. The G-01 accept-side latent hazard is mechanized shut, not just unblocked: both single-stream loops are now the same shape (dispatch-spawn + pending-resolution), the full-duplex loop `serve_single_stream` composes them and is unchanged in its arm semantics (frame-arm equivalence preserved per the non-findings audit). - Write-failure semantics changed from `break` (close the loop) to `warn` (keep reading) in the spawned Once path, matching the Sink arm: a dying transport surfaces on the next read as `ConnectionClosed`; a transient frame-write failure no longer tears down every in-flight request on the connection. **Gates (G-02, productized from the removed probe):** - `hub_handler_composes_peer_announced_op_via_nested_composition` (`src/channels/client.rs`): announce → consumer calls `hub/compose` → the hub's serving loop wire-dispatches it → the handler resolves `consumer/exec` via `ctx.env` nested composition → the forwarding stub's nested `call.requested` crosses back to the consumer. This is the stub path the F-05 gate bypassed. Two wiring facts surfaced (both recorded as spec-consistent, both were silent before): (a) the hub's channel-0 `Connection` must carry the peer identity — `compose_root_env` attaches the connection overlay keyed by `identity.id` (ADR-030 §5), and a deployment that skips identity resolution silently gets no peer overlay (the test sets it via the `AuthContext` the adapter passes to `install_channel_zero`); (b) composition reachability is declared on the composing handler's registration (`scoped_env: ScopedPeerEnv::new(["consumer/exec"])`) — the empty `ScopedPeerEnv` is deny-by-default in `PeerCompositeEnv::invoke_with_policy`, so a wire-dispatched handler with no `scoped_env` composes nothing (the reachability gate, not the overlay, was what the probe's first draft tripped over). - `outbound_call_resolves_while_inbound_subscription_is_being_served` (the optional interleaved-directions phase on the F-04 shape): the consumer subscribes to the hub (Sub served by the consumer's loop), then calls `hub/interleave` over the wire — its handler issues an outbound hub→consumer call on the same connection while the Sub is live — and asserts the call resolves and the Sub is still live after. Both gates bounded at 5s (vs. the 30s sweeper), so a regression fails fast, not via sweeper eviction. **Load-bearing verification (both gates, empirical):** each gate was run against the pre-fix loop (`git stash push src/protocol/dispatch.rs`) and reproduced the G-01 hang: ``` hub_handler_composes…: panicked "nested composition through the forwarding stub timed out (G-01 shape): Elapsed(())" — 5.01s, no progress outbound_call_resolves…: panicked "interleaved outbound call starved while Sub served (G-01 shape): Elapsed(())" — 5.06s, no progress ``` With the fix, both resolve (0.01s / 0.11s wall including connection setup). No sweeper evictions (bounded timeouts ≪ 30s). **Consequences audited against the fix:** - Same-connection nested composition of peer-announced ops works for wire-dispatched parents (the flagship ADR-022 amendment flow) — the gate proves it end-to-end. - The accept-side imported-op composition hazard (G-01's second consequence, latent pre-`f84d214`) is mechanized shut by the pending-resolution arms in `run_loop_single_stream` — not merely unblocked. There is no dedicated gate for this shape yet (the accept-side import + nested-compose e2e would be a further gate; noted as residual work, not a regression). - Head-of-line blocking is gone: spawned arms proceed concurrently; the loop never awaits a handler. - Frame ordering: per-request ordering is preserved by the `SharedFrameWriter` (each `write_frame` is atomic under the mutex); responses for two requests may now interleave *at frame granularity*, which single-stream mode always permitted (both directions' frames are multiplexed by design; correlation is by id). **Verification (post-fix):** ``` cargo test → 583 passed, 0 failed cargo test --all-features → 600 passed, 0 failed cargo clippy --all-targets -- -D warnings → clean cargo clippy --all-features --all-targets -- -D warnings → clean cargo fmt --check → clean (fmt applied) cargo clippy --target wasm32-unknown-unknown -- -D warnings → clean cargo doc --no-deps → clean ``` **Residual (not blocking Unit 1):** the sink-start-inline shape means a `Pub` op whose registration itself blocks (ACL/schema compile are sync and fast; `resolve_sink_handler` runs the handler's ACL path) still holds the loop briefly — bounded by registry work, not handler work. A malicious `AccessControl::check` implementation could stall the loop; that is a deployment-provided trait object, the same trust boundary as `IdentityProvider`, and unchanged from the pre-existing shape. --- ## Verification log (this pass) - All gates in Baseline verification reproduced at tree `f84d214` (clean working tree; the probe was added, run, and removed — `git status` verified clean after removal). - G-01 verified by code trace (the four steps above, each line read at `f84d214`) **and** empirically: probe `probe_hub_handler_composes_peer_announced_op` (in `src/channels/client.rs#[cfg(test)]`, since removed) — hub registry carries `op/register` + `hub/compose` (whose handler invokes the connection overlay's registration for the announced op, the shape `PeerCompositeEnv` → `OverlayOperationEnv` resolution produces); consumer announces `consumer/exec` then calls `hub/compose`. 8s-timeout run: `Elapsed` (no progress). 45s-cap run: resolved at **30.0007s** with `CallError::TIMEOUT` (retryable) — the `DEFAULT_CALL_TIMEOUT` sweeper eviction, not live resolution. - The existing gates' blind spot verified by reading both e2e gates' call directions: `serving_loop_hub_to_consumer_call_resolves` (`client.rs:1373-1399`) and `op_register_announce_then_hub_call_routes_back_to_consumer` (`client.rs:1529`) — the latter's `accept_conn.call("consumer/exec")` crosses the wire to the consumer's local registry; the forwarding stub is unreachable from it. - G-03 verified by reading the collision gate (`op_register.rs:142`) against `PeerCompositeEnv::invoke_with_policy`'s resolution order (`env.rs:230-245`) and `compose_root_env`'s attach (`dispatch.rs:207-214`). - G-04 verified by field audit: `OperationSpec`'s fields (`spec.rs:175-206`) vs `spec_to_json_pub` (`discovery.rs:211-234`) vs `rebuild_spec_for` (`from_call.rs:209-286`). - G-05 verified against `discovery.rs:290-313` and ADR-022 amendment's set (`022-…contract.md:380-393`). - Frame-arm equivalence of `serve_single_stream` vs the two half-loops verified read-through (Non-findings). - alkhttp cross-repo claims verified in `/workspace/@alkdev/alkhttp` at `5b62307` (OQ-05 `open-questions.md:102-112`; ADR-048 `decisions/048-…md:19-25`). - CJK sweep: no CJK-range codepoints in any `.rs`/`.md`/`.toml`/ `.json` under the repo at `f84d214`.