Implements plan 003 W1's API item (ADR-008 Amendment 2, task
tunnels/corrmiss-forwarded-gate): the assembly-injected correlation
gate wrapped into the alkcall OpenEstablisher dance for the forwarded
op's serving half.
- ForwardedGate: sync gate closure over the parsed
TunnelForwardedParams; a gate Err → typed channel:open_failed
before the channel is born (no phantom channel)
- register_tunnel_forwarded_openable_with_establisher: reuses
make_forwarded_op_handler unchanged (additive-only; the base
registration stays byte-for-byte compatible as the
compatibility + residual-race-fallback posture)
- ForwardedPlan: empty marker plan payload (typed-opaque posture,
payload choice pinned); no reply-field contribution
- Establisher parse failure maps TunnelEstablishError::HandlerError
→ channel:open_failed (deliberately pinned improvement over the
base shape's silent birth-teardown for malformed ingress); gate
errors pass through the existing From<TunnelEstablishError> table
(no new variants; stale token = unknown_resource); timeout: None;
wrapper order (schema → ACL → allocate → establisher → handler)
pinned in the establisher doc
- Two-shape posture doc updates: make_forwarded_op_handler's
correlation-miss block, register_tunnel_forwarded_openable's doc;
wire.md §The Forwarded Op + tunnel_forwarded_spec() doc synced
per Amendment 2's "syncs with the API task's in-code doc updates"
Verification: cargo test (85 green, existing suite untouched);
clippy --all-targets -D warnings (native + wasm32); cargo fmt --check;
cargo doc --no-deps (0 warnings); --all-features test run green
- Cargo.toml: alkcall 0.8.1 -> 0.9.0 (crates.io, the stub-sweep
release) and 0.2.1 -> 0.3.0 (breaking dep bump + public alkcall
re-exports -> minor, matching the 0.7.1 -> 0.8.0 ride on 0.2.0)
- fuzz/shared/Cargo.toml: alkcall 0.9.0 (path-linked alktunnels must
see one alkcall version)
- src/producer.rs + tests/producer_direct_op.rs: the only code contact
with the 0.9.0 breaking surface — Establishment no longer derives
Debug; two expect_err calls -> .err().expect, one untyped
panic!("{other:?}") fall-through -> typed Err/Ok arms
- CHANGELOG: 0.3.0 section + compare links
- README: the 0.3.0 note
- docs/architecture/overview.md: dep tree alkcall 0.9.0
Verification: cargo test (136 default / 163 with local), fuzz/shared
corpus replay (0 pass, empty target set — no regressions), clippy
--all-targets -D warnings (native + wasm32-unknown-unknown), fmt
--check, wasm32 check, and cargo publish --dry-run --allow-dirty all
green
The wave-2 stateful target from docs/research/fuzzing.md 3.3: op
sequences (arbitrary SessionSequence) over one live channels stack
per exec - MuxRunner -> raw chunk stream -> the real
run_demux_loop_for_client demux -> ChannelManager per side (producer
Connect / consumer Accept over one duplex transport, the reverse
POC's topology). The harness mirrors TunnelSession's data-plane
shapes: datagram planes (codec reader + pending, the
DatagramHalves mirror) and stream planes (pump_bidi against
in-process peer halves, the pump_against mirror).
Invariants (3.3): no-panic/no-wedge (5s reconcile join); two-pump
shutdown-on-completion; lossless p2c copy count; EOF-sentinel
teardown semantics (drain-before-EOF, map empties); FIFO datagram
reconciliation through the real demux path (stateful framing walk,
the wave-1 walk carried across ops); the 1.3 DatagramHalves.pending
backlog bound (<= routed-but-unpulled remainder); decoder-state
retained-bytes bound re-asserted per op.
Harness-model findings (all six wave findings are harness defects,
not crate bugs - the fuzzer-as-harness-checker pattern):
- duplicate adopt is a legal ChannelExists no-op (documented table)
- the p2c-EOF pump join is the DESIGNED half-close shape; the pump
completes once the c2p source EOFs too (teardown), not before
- route_payload routes into the LOCAL manager (demux direction);
the producer pump writes the channel send half - the first
campaign's bytes never reached the consumer; rewritten to the
real protocol shape (Open allocates, session-index addressing,
stateful framing model, Pull op, EOF sentinels only from
teardown/sender-drop)
- clear_all's return is the opener-ledger records, NOT the reaped
ids (adopted channels carry no ledger entry - the alkcall 7.7
lesson re-learned); the map-empties assert is the ground truth
Campaign (10-min detached, 6 runner): 228,676 execs, 32 fork jobs,
oom/timeout/crash 0/0/0, exit 0. Coverage 3427 edges / 17.5k
features still expanding at budget end. No crate bugs found - the
two-pump contract, EOF-sentinel semantics, and the pending backlog
bound are demonstrated properties.
Crate addition: TunnelSession::datagram_backlog() (the
DatagramHalves.pending diagnostic the 3.3 bound asserts against, in
the retained_bytes() style) + a suite test pinning its rest states.
Verification: main suite 136 passed; corpus replay (8 committed
seeds + 2 semantic fixtures) green; clippy -D warnings + fmt clean
(main, shared, wasm); wasm check clean; cargo fuzz build clean.
Bump the substrate dependency to alkcall 0.8.1 — the bug-fix release
whose fix (duplicate adopt/open no longer destroying the live channel)
is directly relevant to this crate's reverse-path adopt flow.
Adopt fuzzing per docs/research/fuzzing.md, scoped smaller than
alkcall's (the envelope/chunk/demux/manager surfaces are upstream and
now fuzzed there; this crate fuzzes its own named downstream target —
the ADR-003 datagram codec — plus the params/reply parsers):
- fuzz/ workspace per the alkcall pattern: nightly-pinned subtree
(rust-toolchain.toml), thin fuzz_target! binaries, and a
stable-toolchain shared crate carrying the invariant logic so the
committed corpus replays as plain `cargo test` (the fuzz gate in
the release checklist — no hosted CI, alkcall §7.9 posture).
- Targets (both over already-pub APIs; no #[cfg(fuzzing)] exposure):
- datagram_codec — DatagramReader::feed + frame_datagram: no-panic,
infallible decode, size bound, F-2 sentinel layering (empty
datagram only from a length-0 header at a frame boundary),
3-byte-slice splitting transparency, re-frame round-trip,
oversize rejection at the bound, is_mid_datagram framing state,
and the decoder-state retained-bytes bound.
- params_parse — serde params/reply parsers + establishment_reason:
no-panic on raw bytes and bounded structured Values, clean typed
failure, round-trips, listen: 0 pin, loud/lenient projection-pair
agreement, pass-through reason extraction model.
- New public diagnostic: DatagramReader::retained_bytes() (decoder-
state byte count; the fuzz bound asserts against it).
- 96 committed seeds (gen_fuzz_seeds.py) + the campaign-found crash
input promoted as datagram_codec/seed-900; grown corpora gitignored.
- run-detached.sh: setsid+nohup runner so campaigns never share fate
with the agent session process tree (fuzzing.md §5).
Campaign findings this session were harness-model defects, not crate
bugs (both invisible to example-based review; the alkcall
"fuzzer as harness-checker" pattern confirmed):
- the F-2 empty-datagram invariant first counted [00 00] pairs in the
raw stream — a pair inside a payload region is payload, not a
header; corrected to walk the framing (crash artifact saved).
- establishment_reason is a pass-through extractor of details.reason,
not a validated table (harness first asserted a closed set).
- is_mid_datagram means a partial datagram, not buffered header bytes
(a 1-byte header fragment is buffered, not in flight).
Verification: cargo test 135 passed; clippy -D warnings and fmt clean
(main + fuzz/shared); corpus replay green; cargo fuzz build clean;
wasm32-unknown-unknown check clean. Wave-1 campaigns (10-min detached,
fork mode): params_parse 3.14M execs / datagram_codec 827k execs,
0 crashes/OOMs/timeouts; coverage saturated on the codec state space.
- N-3: parse-before-accept telemetry note on make_forwarded_op_handler
(the accepted-flag reads false for a malformed-params open by design)
- N-4: ADR-007 Consequences lines — the untagged-leniency-is-
schema-gated paragraph + the overlapping-variants caution
- N-2: ADR-008 §1 widened to name the any-byte idle-read posture
(the Ok(0)-only reading hangs on a write-happy peer)
- N-5: mint_token's doc comment records the 256-channel cap analysis
- The N-1/N-6/N-7 + G-1/G-3 no-action dispositions recorded in the
task file (the tracker the task asked for)
Verified: fmt, clippy (native + wasm32), test, cargo doc — all green
- D-3: inline the serde-layer check into the renamed
serde_rejects_unknown_substrate_value; delete the overpromising
jsonschema_valid helper (the registry's real schema gate stays
pinned end-to-end by the integration probes)
- D-4: trim the stale Unreleased preamble to a one-liner (written
before the six graduation implementation commits landed)
Verified: cargo fmt --check; cargo clippy --all-targets -D warnings;
cargo test (86 + 2 doc-tests) green
- BindFirstPlan gains substrate: Substrate, stamped at establisher
time from the one successful TunnelParams parse (one parse, one
source of truth; the plan-payload change is pre-consumer)
- make_bindfirst_accept_handler drops the second params re-parse and
the silent unwrap_or(Substrate::Tcp) fallback entirely — the
dispatcher reads plan.substrate; no fallback branch remains, so a
future drift cannot silently stamp Tcp into a UDP listen tunnel's
forwarded opens (the raw-vs-framed data-plane mismatch class)
- tests: forwarded_opens_carry_plan_stamped_substrate pins the plan
as the substrate's source over a real UDP bind-first wire flow
(a substrate-recording forwarded stub observes substrate: udp);
the unit downcast + establisher-flow tests pin the stamp
- docs: producer.md (the plan carries {listener, token, substrate};
the forwarded open's substrate also comes from the plan),
ADR-008 §1 (the stamp + the removed fallback)
- task review-002-m1-substrate-fallback: completed (Notes filled)
Verification: cargo test (145 pass), cargo test --all-features (153),
clippy --all-targets -D warnings (default + all-features), fmt
--check, doc --no-deps, wasm32 check + clippy -D warnings.
- src/consumer.rs: `TunnelListener` — the listen-flow sibling of
`TunnelSession`: `open` rides `open_channel_with_reply` (the U-2
surface, alkcall 0.8.0) and projects the bind-first `bound`/`listen`
reply fields via the lenient `listen_reply_fields`; `bound()` /
`listen()` present them (the Option posture — None for dial/
accept-first producers and old producers). Teardown ownership
(ADR-005's posture, listen-flavored): `close` (graceful — `shutdown`
on the held write half, the explicit EOF sentinel, then the sync
reap) and `Drop` (the held write half drops → the mux pump's
implicit-EOF chunk — the far accept dispatcher's teardown signal —
then the sync reap; never leaks). The held stream is the write half
(`MpscSendStream`) only, IDLE and private — a listen channel has no
data-plane role. No `Clone` (compile_fail doc-test).
`forwarded()` decided: NOT provided — the receipt queue is
assembly-side bookkeeping; the type hands the assembly the token.
- src/lib.rs: re-exports `TunnelListener`; crate docs mention the
listen-flow session.
- src/producer.rs: doc-fix drive-by — two pre-existing broken
intra-doc links (cargo doc is now 0-warning clean).
- tests/harness.rs: `wire_listener_bindfirst` + `ListenerTopology` —
the opener-dials shape (client + serving on the opener; bind-first
serving + the call-plane forwarded-open caller on the producer).
- tests/consumer_listener.rs: 5 integration tests — the bind-first
gate (reply fields + forwarded round-trip through the REAL
forwarded-openable registration + the teardown cascade), the Option
posture on a dial producer, close/Drop reaping (the leak-free
guarantee), the adopt-path pin (`adopt` gains nothing — a forwarded
channel presents the ordinary session semantics), and the scope
interaction (`tunnel:open`-only opener opens the listen channel;
forwarded opens to it fail FORBIDDEN).
- CHANGELOG: Unreleased → Added.
Verification: cargo test 132 passed (157 --all-features; doc tests
2/2 incl. both compile_fail asserts), clippy --all-targets -D warnings
clean (native + wasm32), fmt clean, wasm32-unknown-unknown check
passes (the default crate stays wasm-clean), cargo doc --no-deps clean.
- BindFn + BoundListener: the platform-boundary bind seam (the
protocol never binds, OQ-TN-04); the opaque listener rides the plan
- bindfirst_establisher: registry namespace gate -> token mint
(non-zero-seeded AtomicU32, per-open fresh, per-producer unique,
token 0 never minted — the wrap shape unit-pinned) -> bind ->
`bound` + `listen` reply fields -> the BindFirstPlan plan; the
typed errors pass through (bind failure dial_failed, exhaustion
resource_shortage, registry miss unknown_resource)
- BindFirstAcceptLoop: the type-erased accept closure (receives the
opaque listener; yields (peer, halves) per accept; Err is
loop-critical)
- ForwardedOpener: the per-accept forwarded-open caller — the call
plane (call_with_payload on channel 0 + manager.adopt_channel; the
reverse POC's open_reverse_channel shape)
- make_bindfirst_accept_handler: the accept dispatcher — accept_bi
first (R-02 telemetry), the idle listen-stream EOF sentinel cancels
the loop + tears the listener down (select!), per-accept pump_bidi
tasks tracked by the handler's JoinHandle (awaited on loop exit —
a forwarded pump never outlives its listener), per-accept failure
isolation (log + drop; v1 no retry), loop-critical accept errors
end the loop
- register_tunnel_bindfirst_openable: same spec + tunnel:open scope
gate (the per-registration establisher posture); the injected
seams ride BindFirstInjections {bind, accept_loop, open_forwarded}
(8-arg sketch -> the bundle; clippy too_many_arguments) + the
optional CF-006 identity witness (witness_establisher wrapper)
- harness: RegistrationMode::BindFirst + wire_bindfirst; the consumer
hook gained the accept-side identity propagation + a shared serving
registry (Topology.consumer_registry exposed); the forwarded-op
serving stub (make_forwarded_stub_handler — correlate by token,
accept the BiStream, pump_bidi against the correlated halves; the
opener-side registration lands with tunnels/forwarded-receipt)
- tests: 6 new integration tests (the full flow e2e — the acceptance
gate incl. the byte round-trip through the whole path, the
dial-open neither-field projection, EOF teardown, per-accept
isolation, typed establishment errors, token discipline) + 6 unit
tests (the mint discipline, the establisher flow at the
Establishment surface, the witness wrapper)
Verified: cargo test green (118 default; 138 --all-features), clippy
--all-targets -D warnings clean (native + wasm32), fmt clean,
wasm32-unknown-unknown check passes, cargo doc --no-deps clean.
- DatagramReader::feed is infallible (the oversize bound is enforced
at frame time; the same zombie category as 4014ba6's InvalidLength).
The Vec<Result> shape, the local adapter's dead InvalidData mapping,
and consumer.rs's `?` drop.
- The sender-block machinery test (root-gated veth fixture, auto-skip
without root): the peer address unassigned + the neighbor table
flushed pins the connected sender in a persistent WouldBlock/ENOBUFS
state; the test drives the WouldBlock stash, the readiness-Pending
arms, AND the accepted-byte high-water Pend in one behavior, then
resolves (addr assign + peer bind) and asserts the queued datagrams
drain and deliver. Jumbo MTU (65535) keeps IP fragmentation out of
the way; the block-state probe runs on a throwaway socket (an
oversized probe's partially-built skb chain stays corked on EAGAIN
and poisons the socket).
- pump_queue_to_socket's stash arm now matches ENOBUFS(105) alongside
WouldBlock — the blocked-neighbor state reports 105, and both mean
"the socket cannot take this datagram now".
- connect_udp's resolve-failure arm covered (an RFC 6761 .invalid
target maps to dial_failed; rootless).
- The no-witness tunnel_establisher wrapper covered (a harness
DialNoWitness mode + a full open round-trip through the wrapper).
Negative result pinned in the task doc: tokio 1.53 structurally masks
UDP ICMP errors out of the poll drive (poll_recv_ready/try_io mask
ERROR out of the read direction), so the recv-side ECONNREFUSED
mapping arm is unreachable through the pump's drive — it joins the
defensive list.
Coverage: 92.86% -> 93.91% lines (50 -> 41 uncovered, all deliberate).
Verification: cargo test + cargo test --all-features (91 tests),
clippy --all-targets --all-features, fmt, wasm32 check+clippy,
cargo doc — all clean.
Closes the behavioral test gaps the 2026-09-09 coverage review found
(88.28% → 92.86% lines; details in tasks/tunnels/coverage-weak-spots.md):
- Datagram EOF paths (consumer.rs:377-382, 255) — previously
zero-covered documented contracts: a clean stream EOF resolves
`Ok(None)` (the pump-less session's shutdown signal), a mid-datagram
EOF fails loud with `TruncatedDatagram` (OQ-TN-13), both over a new
closable UDP echo dial (`closing_udp_echo_dial`, both shapes).
- UDP `take_halves` (consumer.rs:201-205) — pins the N-3 framing
contract: taken halves carry raw `[len: u16 BE]` wire bytes, and
the data-plane accessors fail `ChannelTaken` after the take.
- Reverse UDP `pump_against` (consumer.rs:289) — the framed side
round-trips end-to-end (payload + the F-2 empty datagram `len=0`
survives the pump), the N-11 boundary contract.
- `AcceptQueue::push` after close returns `Err(handle)`, closed pop
resolves `None`, and the accept loop's queue-closed mapping ends
the loop with the handler error (producer.rs:279-288, local
accept_loop).
- `establishment_reason` wrapper (params.rs) — unit test over the
typed surface (open_failed reason / non-establishment /
AdoptFailed); plus the schema-gate preemption probe in
producer_open_op (missing substrate is `INVALID_INPUT` before the
establisher runs).
- `UdpHalf` write machinery (local/mod.rs) — four poll-driven tests:
queue accept + flush drain to the wire + shutdown best-effort,
repeated flush cycles, the unconnected-socket error mapping
(deterministic EDESTADDRREQ), and the shutdown-drain-swallows-
errors posture. Host probes recorded in the task doc: no UDP
sender-blocking shape is reachable on this host (loopback/veth
drop silently; the fresh-socket try_send quirk is timing-tolerated,
never asserted), so the stash/high-water paths stay uncovered by
host limitation, not by oversight.
Verified: cargo test (default + --all-features), clippy --all-targets
-D warnings, fmt --check, wasm32 check + clippy, llvm-cov 92.86%.
The coverage review found `InvalidLength {declared, have}` is a
zombie: `DatagramReader::feed` never returns `Err` — it buffers
partial frames indefinitely, and the truncated-stream contract is
carried by `is_mid_datagram` (the codec diagnostic) plus
`TunnelIoError::TruncatedDatagram` (the session surface). The
variant's doc claimed it was the truncated-stream error; no spec
(ADR-003, wire.md, bast.md) names it, and no external consumer
exists (verified 2026-09-08 per ADR-005's N-3 amendment), so the
API window is open — removed before the first consumer pins it.
Also removes the `unreachable!` arm its removal strands in the
`local` adapter's recv mapping (a no-panic path; single-variant
destructure now), and records the planned-shape correction in the
completed wire-codec task doc.
Verified: cargo test (--all-features and default), clippy
--all-targets -D warnings, fmt --check, wasm32 check + clippy — all
clean.
Review 001 remediation (tasks/tunnels/fix-udp-framed-adapter):
- U-1 [major]: UdpHalf now composes the ADR-003 codec at the
substrate boundary — poll_write de-frames channel bytes (incremental
DatagramReader) and sends each payload as one raw datagram;
poll_read frames raw target datagrams and hands frame bytes to the
pump. Targets see payload-only bytes; empty datagrams round-trip
(the F-2 fix end-to-end); >8 KiB datagrams arrive as ONE datagram.
Mechanics the verification gates forced beyond the sketch:
poll_write accepts all offered bytes (tokio copy re-offers a full
window unsatisfied — partial-frame holds deadlock >8 KiB frames),
backpressure is an accepted-byte high-water mark, socket drain
pumps on write/flush/shutdown; >caller-buffer frames stage across
reads (the truncation probe's assertion surface updated per the
re-validation correction).
- U-2 [minor]: connect_udp resolves the target and binds the
unspecified address of the matching family (v6 targets dial from
[::]:0); bind_tcp gains the wildcard-exposure doc warning.
- U-3 [minor]: the 65535 recv scratch allocates lazily on first recv
(full-size read kept — try_recv truncates silently; the MTU-shrink
idea confirmed unsafe in re-validation).
- N-1: the module/connect_udp/UdpHalf docs now describe the composed
codec truthfully.
Gates (permanent tests in tests/local_halves.rs): recording-target
suite (payload-only at the target, empty-datagram round-trip, 9000-
byte single datagram, unframed-target resolution), the 65507-byte
max-datagram staging probe, and the v6 dial gate.
Docs: ADR-003 §Amendment (the boundary mechanics), wire.md truncation
bullet, consumer.md adopt sketch (substrate param). Review 001
resolution notes on U-1/U-2/U-3/N-1; task marked completed.
Verified: 63 default / 76 local tests ×3 repeat runs; clippy
-D warnings (all-targets + wasm32), fmt --check, doc --no-deps
(warning-free), wasm check, publish dry-run.
Findings + review doc: docs/reviews/001-implementation-review.md (the
alkcall house pattern). Closes tunnels/review-core-crates and
tunnels/review-impl.
Findings:
- U-1 [major, NOT fixed] — the local UDP adapter is not the framed
adapter ADR-003 mandates: codec-framed bytes reach real targets,
empty datagrams are EOF-shaped at the adapter (tunnel teardown),
>8 KiB datagrams split, unframed target-initiated datagrams are
dropped. Executable-pinned by real-socket probes (temporary test,
deleted after the run). Remediation task: tunnels/fix-udp-framed-adapter.
- C-1 [major, fixed] — AcceptQueue::pop lost wakeup in the
check→register window (open op hangs to the establishment bound);
fixed via the tokio notify_waiters contract (Notified created before
the check each iteration) + multi-thread regression test.
- N-4/N-5/N-7/N-8/N-9/N-10/N-11 [minor, fixed] — serialize TunnelParams
in open_reverse_channel; stale doc ref; broken doc link; root
re-exports (TunnelParams/Substrate/tunnel_open_spec/OP_TUNNEL_OPEN);
README (publish dry-run was failing on the missing readme); harness
comment; pump_against UDP framing doc note.
- N-2 retracted by executable falsification (codec buffering is
bounded by construction); N-6 non-finding with rationale.
Verified: 63 tests default / 70 with --features local, 3x repeat-run
clean both configs; clippy -D warnings (all-targets + wasm32); fmt;
doc warning-free; wasm32 check; cargo publish --dry-run passes.
- src/local/mod.rs (feature = "local" -> tokio/net): dial_tcp (5s
timeout), connect_udp (ephemeral bind + connect + the FRAMED UdpHalf
adapter), dial_unix (OQ-TN-14 in v1), bind_tcp +
TcpListenerHalves::accept_loop (the listen shape's assembly half),
local_dial() (one DialFn covering all three substrates).
- UdpHalf truncation fails loud (OQ-TN-13): poll_read recvs into a
scratch buffer first and size-checks against the caller's buffer —
an over-size datagram is io::ErrorKind::InvalidData, never a silent
truncation (tokio's poll_recv-into-ReadBuf would truncate).
- No socket type appears outside src/local/ (convention 16); the
default crate stays wasm-clean; stdio bridging is NOT here (alktty
owns process stdio).
- Tests (tests/local_halves.rs, 7, feature-gated): TCP/UDP/unix dial
round-trips through the real establisher path, the 1400-byte MTU
datagram, the truncation fail-loud probe, dial-refusal ->
dial_failed, listen producer over a real TCP listener end-to-end.
Verified: cargo test green (default + --all-features, 44 + 7),
clippy -D warnings (default + all-features + wasm32), fmt clean,
cargo check --target wasm32-unknown-unknown passes.
- AcceptFn: the injected accepted-connection source (the assembly
layer owns the listener + accept loop; the protocol never binds —
OQ-TN-04).
- AcceptQueue: the protocol-side queue contract (async push/pop/
close; FIFO always-before-take ordering; close-while-waiting
resolves None). Empty-queue posture is assembly-owned (a late
accept is legitimate; resource_shortage is the closure's mapping;
the wrapper's 10s establishment timeout is the backstop).
- listen_establisher: same open op, same params, same typed errors —
registry namespace gate → accept() → Establishment::new(plan);
the pump handler is untouched (plan-flow with a different source).
- register_tunnel_listen_openable: same spec/pump registration with
the listen establisher (the honest shape vs a dial/accept enum:
the establisher is the only difference; one establisher per op id
per session registry — documented).
- Tests (tests/producer_listen.rs, 6): listen flow end-to-end, FIFO
ordering across two opens (R-01 plan-flow, listen-flavored), empty
queue -> resource_shortage, closed listener -> dial_failed,
unknown resource -> unknown_resource, late-push wait-then-resolve.
- Harness: RegistrationMode enum + wire_listen.
- TargetHandle gains a structural Debug impl (test ergonomics).
Verified: cargo test green (44), clippy -D warnings (native + wasm32),
fmt clean, wasm32 check passes.