Files
alkcall/docs/reviews/005-serving-loop-concurrency-and-op-register-review.md
T
glm-5.3-flash d5b2661b38 fix(review 005 Unit 3): resource_id_path wire round-trip + bootstrap-list doc alignment (G-04, G-05)
- resource_id_path rides both halves of the spec wire round-trip:
  spec_to_json_pub serializes it (optional string key), rebuild_spec_for
  parses it. Additive optional field - absent stays absent. Previously
  an announced (or from_call-imported) op declaring ownership-scoped
  resource extraction silently rebuilt with resource_id: None, so ACL
  checks ran without the resource ID.
- Gates: spec_round_trips_resource_id_path (serialize -> parse ->
  field intact) + spec_without_resource_id_path_stays_absent (additive
  field breaks no consumer).
- ADR-022 amendment: bootstrap-op set gains services/list-peers with a
  dated G-05 note (the installer has registered it since the amendment
  landed; the doc lagged the code). Set remains closed at four.

Verification: cargo test 589 / --all-features 606, clippy
(all-targets, all-features, wasm32) clean, fmt clean, doc clean.

Refs docs/reviews/005-...md (G-04, G-05; all findings closed).
2026-09-04 09:41:43 +00:00

704 lines
35 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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<OperationRegistry>`); 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<OperationRegistry>` 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<parking_lot::Mutex>` 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`.