- 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).
704 lines
35 KiB
Markdown
704 lines
35 KiB
Markdown
# 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`. |