docs: step-3 rewrites — OQ-39/40 resolved, C-08 closed, channel-operations.md put/control/resources contracts, review 010/011 closed with the S-01 sketch corrected

- open-questions.md: OQ-39 → ADR-052, OQ-40 → ADR-053 (resolutions replace
  the deferred-until-needed posture)
- channel-operations.md: the put contract (initiator allocates + streams,
  reserved input key, one response, acks-in-result sentence), the control
  routing (ADR-052), the enumerator aggregation (ADR-053), the
  connection-owner paragraph's pub-era correction, the ADR table rows
- ADR-051 §6: PubTypedOpen → the put translation; ADR-047's 2026-09-06
  amendment records its era honestly (the dynamic half is now ADR-053)
- review 010: all four findings resolved (with the S-01 sketch correction
  so the published record stops carrying the wrong-graph reading)
- review 011: closed (all seven pins implemented + gates landed)
- review 001 C-08: resolved note (odd/even + adopt, implemented)
- AGENTS.md: the 'Stub Guard' section — the not_implemented grep is part
  of cargo test; the registration invariants and the implement-or-stop
  ruling are release-blocking

Verified: cargo test (696+guard), clippy -D warnings, fmt, doc clean.
This commit is contained in:
glm-5.3-flash committed 2026-10-13 09:04:13 +00:00
1 parent 944c300c17
commit 3317ea0963
10 files changed
+205 -43

No files matched your search

+22
View File
@@ -213,6 +213,28 @@ cargo clippy --target wasm32-unknown-unknown -- -D warnings
cargo semver-checks check-release # before a release cargo semver-checks check-release # before a release
``` ```
## Stub Guard (release-blocking)
Four registered-but-always-failing stubs shipped across nine published
versions (reviews 010/011). This class is now release-blocking in both
directions:
1. **Test guard**: `cargo test --test stub_guard` (part of `cargo
test`) fails if the string `not_implemented` appears anywhere in
`src/`. A "loud failure" stub is still a stub.
2. **Registration-time invariants** (ADR-047 amendment 4 §4.8): a
marked (`channel_open`) spec refuses the generic registration path
(`OperationRegistry::register`, `CallConnection::register_imported`,
`op/register`) and refuses `publish_schema`; the sanctioned
registrations are the channel wrapper family
(`register_openable*`, `register_pub_openable`,
`register_relay_openable`).
3. **The ruling**: implement-or-stop-advertising before any publish.
The only permitted deferral statement is "not this session" — never
"next version", "not v1", or any bucket that parks a decision on an
unknown future release. Every handler that registers on a
discoverable surface must be capable of succeeding.
## Architecture Context ## Architecture Context
- `docs/architecture/` — the authoritative spec. Read it before - `docs/architecture/` — the authoritative spec. Read it before
+3 -1
View File
@@ -13,7 +13,7 @@ This crate unifies `alknet-call` and `alknet-channels` from the alknet
mono-repo, plus the vendored core types formerly in `alknet-core`. The mono-repo, plus the vendored core types formerly in `alknet-core`. The
source architecture docs were ported from source architecture docs were ported from
`/workspace/@alkdev/alknet/docs/architecture/` and renumbered as alkcall `/workspace/@alkdev/alknet/docs/architecture/` and renumbered as alkcall
ADRs (ADR-001..051). The ALPN strings (`alk/call`, `alk/channels`) ADRs (ADR-001..053). The ALPN strings (`alk/call`, `alk/channels`)
are wire-stable and unchanged — see ADR-004. are wire-stable and unchanged — see ADR-004.
## Documents ## Documents
@@ -104,6 +104,8 @@ are wire-stable and unchanged — see ADR-004.
| [049](decisions/049-channel-open-establishment-phase.md) | Channel-Open Establishment Phase | `OpenEstablisher` + `register_openable_with_establisher` (awaited, bounded); typed `channel:open_failed` with `details.reason`; `Establishment.plan` (`ChannelPlan`) threaded to the `OpenHandler` (amendment 2); the `JoinHandle` data-plane lifetime contract | | [049](decisions/049-channel-open-establishment-phase.md) | Channel-Open Establishment Phase | `OpenEstablisher` + `register_openable_with_establisher` (awaited, bounded); typed `channel:open_failed` with `details.reason`; `Establishment.plan` (`ChannelPlan`) threaded to the `OpenHandler` (amendment 2); the `JoinHandle` data-plane lifetime contract |
| [050](decisions/050-pump-bidi-two-pump-helper.md) | `pump_bidi` Two-Pump Helper | `channels::pump_bidi` — shutdown-on-completion two-pump data plane pinned in one place (alknet ADR-078) | | [050](decisions/050-pump-bidi-two-pump-helper.md) | `pump_bidi` Two-Pump Helper | `channels::pump_bidi` — shutdown-on-completion two-pump data plane pinned in one place (alknet ADR-078) |
| [051](decisions/051-channel-relay-and-hub-leg.md) | In-Tree Channel Relay (`ChannelRelay`) and the Hub-Leg Assembly | ADR-042 amended — the relay implementation is an alkcall export; translate hop = ADR-049 establisher (`register_relay_openable`), byte-forward hop = `pump_bidi`; implicit per-channel id mapping; two-phase registration (discover/stash → fork-register); hub-leg install template; ACL layering note | | [051](decisions/051-channel-relay-and-hub-leg.md) | In-Tree Channel Relay (`ChannelRelay`) and the Hub-Leg Assembly | ADR-042 amended — the relay implementation is an alkcall export; translate hop = ADR-049 establisher (`register_relay_openable`), byte-forward hop = `pump_bidi`; implicit per-channel id mapping; two-phase registration (discover/stash → fork-register); hub-leg install template; ACL layering note |
| [052](decisions/052-channel-control-surface.md) | `channel/control` Control-Handle Surface | Establisher-contributed `ChannelControl` (`Establishment::with_control`), wrapper-installed/teardown-removed in the manager; `channel:no_control_handle` capability answer; resolves OQ-39 |
| [053](decisions/053-resource-enumerators.md) | Live Resource Enumerators | `ResourceEnumerators` (peer-shared registry); snapshot-then-changes enumerator contract; scan-once aggregation; empty seed; resolves OQ-40 |
## Relevant Open Questions ## Relevant Open Questions
+118 -27
View File
@@ -1,6 +1,6 @@
--- ---
status: draft status: current
last_updated: 2026-08-12 last_updated: 2026-10-13
--- ---
# channel-operations.md — Channel Lifecycle on the Call Protocol # channel-operations.md — Channel Lifecycle on the Call Protocol
@@ -60,15 +60,18 @@ Response (`call.responded`):
|-------|------|---------| |-------|------|---------|
| `channel_id` | u32 | Allocated by the connection owner (ADR-047 §5). In the `Sub` case, the responder (producer). Both sides route chunks with this ID to the new channel. | | `channel_id` | u32 | Allocated by the connection owner (ADR-047 §5). In the `Sub` case, the responder (producer). Both sides route chunks with this ID to the new channel. |
**`channel_id` allocation (ADR-047 §5): connection-owner allocates.** **`channel_id` allocation (ADR-047 §5, as amended for the put path by
ADR-047 amendment 4): connection-owner allocates.**
The side that holds the `ChannelManager` for that connection allocates The side that holds the `ChannelManager` for that connection allocates
the `channel_id` via a monotonic `AtomicU32` (`next_id.fetch_add(1, the `channel_id` via a monotonic `AtomicU32` (odd/even split — connect
Relaxed)`) and returns it in the response. In the `Sub` case the side starts at 1, accept at 2) and returns it in the response. In the
responder owns the connection and allocates; in the `Pub` case the `Sub` case the responder owns the connection and allocates — one
initiator owns and allocates. One round-trip before data flows — the round-trip before data flows (the same round-trip every call makes).
same round-trip the call protocol makes for every operation. All In the `Pub` case (the put contract above) the initiator owns,
current channel types (TTY, tunnel, SSH) already require a negotiation allocates, and streams bytes **before** the open resolves — the
round-trip, so the open round-trip is not additive latency. early-arrival parking covers the adopt race. All current channel
types (TTY, tunnel, SSH) already require a negotiation round-trip, so
the open round-trip is not additive latency.
**Error codes** (`CallError.code` strings): **Error codes** (`CallError.code` strings):
@@ -110,19 +113,80 @@ the existing teardown machinery, unchanged. Head-of-line safety: the
serving loop spawns Once invocations as independent tasks, so a slow serving loop spawns Once invocations as independent tasks, so a slow
establisher on one open op does not block other calls on channel 0. establisher on one open op does not block other calls on channel 0.
### `channels/<alpn>/pub` — publish a binary stream ### `channels/<alpn>/pub` — publish a binary stream (the put contract, ADR-047 amendment 4)
`OperationType::Pub` (ADR-046). The initiator publishes a stream of A marked `Pub`-typed open op is the **put** shape: the data plane is
`call.published` events to the responder; the responder's `SinkHandler` the adopted channel's `BiStream` — never the call plane. The contract,
consumes them and returns a single `call.responded`. The pinned end-to-end (ADR-047 amendment 4; review 011's corrections are
`channel_open` marker on the op spec tells the channels layer the the authoritative shape — a serving-loop `PublishStream` sketch was
stream is binary — the `call.published` chunks carry binary payloads considered and rejected, it is ADR-046's *unmarked* pub semantics):
on a data channel (not JSON on channel 0).
1. **The initiator allocates and writes immediately.** The putter
allocates the channel id on its own side (connect side → odd, per
the ADR-047 §5 odd/even split), inserts the **reserved input key
`"channel_id"`** into the open op's input, and streams bytes on the
channel before the open resolves — the early-arrival parking
(ADR-051's tested pre-adopt buffer) covers the responder-adopts
race. Zero `call.published` on marked ops: chunks on the call plane
are rejected loudly with `channel:publish_chunks_rejected` (the
first chunk writes the rejection in the initiator's direction; the
handler is never fed). An unmarked `Pub` op is an ordinary ADR-046
sink (JSON chunks, `publish_schema`); it does not ride the channel
machinery.
2. **The responder adopts.** `ChannelCore::register_pub_openable` is
the only registration seam: the wrapper strips the reserved key
pre-validation (dispatch layer, before `input_schema`) and adopts
the given id (`adopt_channel` — the remote parity range enforced;
failure replies `channel:open_failed` with reason `adopt_failed`).
3. **The wrapper is the responder of record:** `check_open` → adopt →
bounded establisher (offer validation + ack fields +
`Establishment::with_control`, ADR-049 amendment 3's mechanism) →
the `PubOpenHandler` runs on the adopted channel's `Connection`,
**awaited inline** → the standard completion teardown → **one
response**, merge order pinned:
```
{ "channel_id": <adopted> } → ack fields → handler result
```
(the handler result wins on key collision — its computed result
postdates the ack; a handler `Err` IS the response, and the channel
tears down).
4. **The ack-timing sentence (protocol semantics):** a sink op gets
exactly one `call.responded`, produced after the handler completes
— a putter stream-waits nothing; the establishment ack is
informative-in-result, never a gating ack. The fetch/put asymmetry
is direction-inherent: fetch (`Sub`) acks at establishment (the
`{channel_id}` response precedes any byte); put (`Pub`) acks in the
result. A putter that waits for the ack deadlocks — this sentence
is the contract, not per-consumer folklore.
5. **The putter-facing client shape:**
`ChannelClient::open_pub_channel(operation_id, input, alpn)`
allocates the odd id, sends the open op in the background, and
returns `(id, send, recv, reply)` immediately; `reply.resolve()`
is the awaited merged response.
**Error codes** (the pub path):
| code | meaning | retryable |
|------|---------|-----------|
| `channel:open_failed` | establishment failure (ADR-049 §3 vocabulary) or adoption failure — new reason `adopt_failed` (collisions, cap, out-of-range reserved ids; ADR-047 amendment 4 §4.4) | false |
| `channel:publish_chunks_rejected` | `call.published` chunks on a marked op (the data plane is the channel, ADR-047 amendment 4 §4.1) | false |
| `INVALID_INPUT` | absent/non-u32 reserved `channel_id` key on a marked pub op (ADR-047 amendment 4 §4.3) | false |
The hub-as-proxy pattern (ADR-042, ADR-047 §1) composes on top: a The hub-as-proxy pattern (ADR-042, ADR-047 §1) composes on top: a
worker publishes to the hub, the hub owns the resource, and consumers worker publishes to the hub, the hub owns the resource, consumers
subscribe from the hub. The broker (topic registry, `Pub`↔`Sub` subscribe from the hub. Marked pub openables relay through the put
matching) is a hub concern, not an alkcall concern (ADR-047 §1, Gap B). translation (ADR-047 amendment 4 §4.7): the hub allocates its own
connect-side id, opens the spoke op in the background (put
acks-in-result — an establisher awaiting the spoke's response would
deadlock the leg), pumps consumer bytes → spoke leg, and mirrors the
spoke's single response with its reserved key stripped.
Registration invariants (ADR-047 amendment 4 §4.8 — the stub-class
guard): a marked spec refuses the generic registration path and
refuses `publish_schema`; `register_openable*` refuse `Pub`-typed
specs; `register_pub_openable` refuses unmarked/non-`Pub` specs.
## The generic ops (keyed by `channel_id`) ## The generic ops (keyed by `channel_id`)
@@ -159,7 +223,7 @@ TTY's 5-byte payload format); for tunnels it is the last data byte
before close. This invariant crosses two channels (the data channel and before close. This invariant crosses two channels (the data channel and
channel 0), so the channels layer owns the ordering guarantee. channel 0), so the channels layer owns the ordering guarantee.
### `channel/control` — out-of-band control on channel 0 ### `channel/control` — out-of-band control on channel 0 (implemented, ADR-052)
For control that doesn't need ordering relative to data (resize, signal, For control that doesn't need ordering relative to data (resize, signal,
keepalive): keepalive):
@@ -174,11 +238,23 @@ keepalive):
} }
``` ```
The channels layer routes `message` to the handler's control handle for The channels layer routes `message` to the channel's control callback
`channel_id`. The `message` JSON is ALPN-specific; the channels layer (ADR-052): the ALPN crate contributes it through its open-op
does not interpret it. establisher (`Establishment::with_control`); the wrapper installs it
under the allocated/adopted id and every teardown path removes it. The
`message` JSON is ALPN-specific; the channels layer does not interpret
it. The reply is `{ "control": <callback output> }`, or:
### `channel/resources/subscribe` — live resource discovery | code | meaning |
|------|---------|
| `channel:unknown_channel` | no such channel (pre-existing class) |
| `channel:no_control_handle` | the channel's ALPN registered no control surface — a capability answer, not a failure (ADR-052 §3); data-ordered control rides the data channel (DP-4 below) |
| (callback's own code) | the callback's `CallError` verbatim |
Control that MUST be ordered relative to data rides the data channel's
`BiStream` (the control-message division below is unchanged).
### `channel/resources/subscribe` — live resource discovery (implemented, ADR-053)
**This is a `Sub` operation (ADR-021), not a polled Query.** The call **This is a `Sub` operation (ADR-021), not a polled Query.** The call
protocol has `StreamingHandler` / `invoke_streaming` (implemented and protocol has `StreamingHandler` / `invoke_streaming` (implemented and
@@ -193,8 +269,21 @@ start/stop.
} }
``` ```
The responder registers a `StreamingHandler` that emits a Each openable ALPN crate provides a **resource enumerator**
`ResponseEnvelope` whenever the resource set changes. Each event: (ADR-053 §1 — snapshot-then-changes: the first item is the current
payload, subsequent items are full replacements; the producing crate
owns its change detection) at assembly/install time:
`ResourceEnumerators::register(alpn, enumerator)`, one shared registry
per accepting peer. The handler aggregates the enumerators registered
at subscription start (the **scan-once** pin — a later registration is
invisible to an in-flight subscription; re-subscribe):
- **non-empty registry:** each enumerator item becomes one event,
`{ "output": { "resources": [ { "alpn": …, …payload } ] } }` — the
first events across the ALPNs are the initial snapshot;
- **empty registry:** exactly one `{ "resources": [] }` event, then
the stream stays open (subscribe, read the first event, cancel —
the poll use case).
```json ```json
{ {
@@ -471,6 +560,8 @@ All design decisions are documented as ADRs in [decisions/](decisions/).
| [021](decisions/021-streaming-handler-for-subscriptions.md) | StreamingHandler | The machinery `channel/resources/subscribe` uses | | [021](decisions/021-streaming-handler-for-subscriptions.md) | StreamingHandler | The machinery `channel/resources/subscribe` uses |
| [046](decisions/046-publish-operation-type-and-handler-kind-sink.md) | Pub Operation Type | The `Pub`/`Sub` primitives the per-ALPN open ops build on | | [046](decisions/046-publish-operation-type-and-handler-kind-sink.md) | Pub Operation Type | The `Pub`/`Sub` primitives the per-ALPN open ops build on |
| [049](decisions/049-channel-open-establishment-phase.md) | Channel-Open Establishment Phase | The establisher hook; typed `channel:open_failed`; `Establishment.plan` (amendment 2); the `JoinHandle` lifetime contract | | [049](decisions/049-channel-open-establishment-phase.md) | Channel-Open Establishment Phase | The establisher hook; typed `channel:open_failed`; `Establishment.plan` (amendment 2); the `JoinHandle` lifetime contract |
| [052](decisions/052-channel-control-surface.md) | `channel/control` Control-Handle Surface | The establisher-contributed `ChannelControl`; wrapper-install/teardown-removal; `channel:no_control_handle` |
| [053](decisions/053-resource-enumerators.md) | Live Resource Enumerators | The snapshot-then-changes enumerator contract; scan-once aggregation; the empty seed |
| [050](decisions/050-pump-bidi-two-pump-helper.md) | `pump_bidi` Two-Pump Helper | The data-plane helper openable-ALPN handlers await inline | | [050](decisions/050-pump-bidi-two-pump-helper.md) | `pump_bidi` Two-Pump Helper | The data-plane helper openable-ALPN handlers await inline |
| [026](decisions/026-forwarded-for-identity.md) | Forwarded-For Identity | The auth chain for hub-relayed opens (and why the cap is per direct-caller, not per `forwarded_for`) | | [026](decisions/026-forwarded-for-identity.md) | Forwarded-For Identity | The auth chain for hub-relayed opens (and why the cap is per direct-caller, not per `forwarded_for`) |
| [011](decisions/011-dynamic-resource-ownership-for-runtime-spawned-resources.md) | Dynamic Resource Ownership | The parallel — a channel slot is a resource, the cap is a quota check; `resource_id_path` works again under per-ALPN ops | | [011](decisions/011-dynamic-resource-ownership-for-runtime-spawned-resources.md) | Dynamic Resource Ownership | The parallel — a channel slot is a resource, the cap is a quota check; `resource_id_path` works again under per-ALPN ops |
@@ -366,7 +366,9 @@ half load-bearing for the first time and asked for a decision on the
static half. Decision: **both halves of the "static per-op" split gain static half. Decision: **both halves of the "static per-op" split gain
what is cheap today; the dynamic half stays deferred** (OQ-40, now what is cheap today; the dynamic half stays deferred** (OQ-40, now
with a "load-bearing for alktunnels discovery UI" note in with a "load-bearing for alktunnels discovery UI" note in
`open-questions.md`; alktunnels v1 uses config-known op names): `open-questions.md`; alktunnels v1 uses config-known op names).
*[Amendment 4 era, 2026-10-13: the dynamic half is now implemented —
ADR-053; this 2026-09-06 text records the state at its writing.]*
- `OperationSpec` gains `description: Option<String>` — a human- - `OperationSpec` gains `description: Option<String>` — a human-
readable op description, set via `with_description` at registration. readable op description, set via `with_description` at registration.
@@ -205,11 +205,13 @@ same pin.
### 6. Bounds: rejected shapes and residual notes ### 6. Bounds: rejected shapes and residual notes
- **Pub-typed open ops** — rejected loudly (`channel:pub_open_not_implemented`, - **Pub-typed open ops** — ~~rejected loudly~~ **translated** (ADR-047
the C-08 blocker). The relay registers only `Query`/`Mutation`-typed amendment 4 §4.7 — the put-forwarding translation: the hub allocates
ops (plain forwarding) and `Sub`-typed marked ops (relay openables); its connect-side id, opens the spoke op in the background,
a discovered `Pub`-typed marked spec is an assembly error, not a pumps the consumer bytes → spoke, and mirrors the spoke's single
silent stub. response with its reserved key stripped; `PubTypedOpen` is removed).
The rejection remains for the genuinely-unsupported shapes: unmarked
specs (`UnmarkedSpec`) only.
- **Compounding establishment bounds** — each hop's establisher is - **Compounding establishment bounds** — each hop's establisher is
bounded independently (ADR-049 §2); an N-hop chain adds N bounds. bounded independently (ADR-049 §2); an N-hop chain adds N bounds.
The consumer's dispatch deadline (`min(deadline, 10s)` per hop) The consumer's dispatch deadline (`min(deadline, 10s)` per hop)
@@ -263,7 +265,6 @@ same pin.
`ChannelRelay` instead of re-deriving the hub shape. `ChannelRelay` instead of re-deriving the hub shape.
- U-1's gate 2 (the review's silent-failure scenario — flavor-form ops - U-1's gate 2 (the review's silent-failure scenario — flavor-form ops
relaying as plain forwarding stubs) becomes a permanent e2e gate. relaying as plain forwarding stubs) becomes a permanent e2e gate.
**Negative:** **Negative:**
- New pub API (`ChannelRelay`, the registration entry point, the - New pub API (`ChannelRelay`, the registration entry point, the
+4 -4
View File
@@ -1,6 +1,6 @@
--- ---
status: draft status: current
last_updated: 2026-08-12 last_updated: 2026-10-13
--- ---
# Open Questions # Open Questions
@@ -82,8 +82,8 @@ is the load-bearing piece the broker composes on.
| OQ-36 | `channel_id` allocation in Pub case | resolved | medium | ADR-047 §5 — "connection owner allocates" (the side that holds the `ChannelManager`); amends "responder allocates" | | OQ-36 | `channel_id` allocation in Pub case | resolved | medium | ADR-047 §5 — "connection owner allocates" (the side that holds the `ChannelManager`); amends "responder allocates" |
| OQ-37 | `from_call` relay wrapper for marked ops | open | medium | ADR-047 §1 names it as a consumer (hub) concern; alkcall's `from_call` reconstructs the marker (Gap F resolved) so the consumer can branch on it | | OQ-37 | `from_call` relay wrapper for marked ops | open | medium | ADR-047 §1 names it as a consumer (hub) concern; alkcall's `from_call` reconstructs the marker (Gap F resolved) so the consumer can branch on it |
| OQ-38 | ALPN→path-segment mapping | resolved | low | ADR-047 §"Negative" — strip the `alk/` prefix; ALPNs without that prefix use the full ALPN string (rare, two-way-door) | | OQ-38 | ALPN→path-segment mapping | resolved | low | ADR-047 §"Negative" — strip the `alk/` prefix; ALPNs without that prefix use the full ALPN string (rare, two-way-door) |
| OQ-39 | `channel/control` control-handle surface | open | medium | The `channel/control` handler currently returns `channel:control_not_implemented`. The control-handle surface (per-channel control callbacks registered by ALPN crates, routing `message` to the handler's control handle for `channel_id`) is real design work — each ALPN crate needs a way to register a control callback, and the channels layer needs a control-handle registry keyed by `channel_id`. Deferred until an ALPN crate (TTY, tunnel) needs out-of-band control. | | OQ-39 | `channel/control` control-handle surface | resolved | medium | ADR-052 — the control-handle surface is implemented: per-channel control callbacks contributed by the ALPN establisher (`Establishment::with_control`), wrapper-installed and teardown-removed in the manager; the op routes the ALPN-specific `message` and answers `channel:no_control_handle` when the channel's ALPN registered none. The stub (`channel:control_not_implemented`) was removed with the single-publish fix (reviews 010 S-02 / 011). |
| OQ-40 | `channel/resources/subscribe` live subscription | open | medium | The `channel/resources/subscribe` handler currently returns `channel:resources_not_implemented`. The live subscription aggregated from ALPN-crate resource enumerators (ADR-047 §6) requires each ALPN crate to provide a resource enumerator, and the channels layer to aggregate them into a live `Stream` that emits on any change. The current stub is a one-shot error; the real implementation is deferred until a consumer (hub, dashboard) needs live resource discovery. **Load-bearing for alktunnels' discovery UI** (review 006 E-02): a consumer cannot distinguish produced tunnel resources by op name alone. The static half is covered (0.5.0: `OperationSpec.description` on `services/list` — describes the op, not the live resource set); the dynamic half stays deferred — alktunnels v1 uses config-known op names. | | OQ-40 | `channel/resources/subscribe` live subscription | resolved | medium | ADR-053 — the live subscription is implemented: one `ResourceEnumerator` per openable ALPN (provided by the producing crate, snapshot-then-changes), aggregated by the handler over the scan-once snapshot; an empty registry emits one `{ "resources": [] }` seed and stays open. The one-shot error stub was removed with the single-publish fix (reviews 010 S-03 / 011). The alktunnels discovery-UI note (review 006 E-02) is served by the enumerator surface. |
| OQ-41 | QUIC-native multi-stream substrate | open | medium | Only the in-line substrate mode is implemented (single bidi stream, header-demuxed N channels). The QUIC-native multi-stream substrate (accept remaining bidi streams, read headers off each — ADR-034 §substrate modes) is deferred to the downstream alknet crate. The wire format and demux loop are correct for both substrates; only the outer `accept_bi()` loop is missing. The alknet crate owns the QUIC dial/accept loop and is the natural place for the multi-stream accept loop. This crate stays transport-agnostic (no QUIC dependency, WASM-compatible). | | OQ-41 | QUIC-native multi-stream substrate | open | medium | Only the in-line substrate mode is implemented (single bidi stream, header-demuxed N channels). The QUIC-native multi-stream substrate (accept remaining bidi streams, read headers off each — ADR-034 §substrate modes) is deferred to the downstream alknet crate. The wire format and demux loop are correct for both substrates; only the outer `accept_bi()` loop is missing. The alknet crate owns the QUIC dial/accept loop and is the natural place for the multi-stream accept loop. This crate stays transport-agnostic (no QUIC dependency, WASM-compatible). |
## Core Types ## Core Types
@@ -962,7 +962,11 @@ promises.
vice versa) as the smaller change; record the decision in ADR-047 §5 vice versa) as the smaller change; record the decision in ADR-047 §5
or a follow-on ADR. Then implement the adoption path: the or a follow-on ADR. Then implement the adoption path: the
non-allocating side registers the remotely-assigned id in its non-allocating side registers the remotely-assigned id in its
manager/mux. manager/mux. **[RESOLVED 2026-10-06/13: the implemented odd/even
split (connect odd, accept even) + `ChannelManager::adopt_channel`;
the marked-pub contract rides it end-to-end — ADR-047 amendment 4;
the C-08-blocker stub class this decision previously gated on is
closed with reviews 010/011's single-publish fixes.]**
- C-12: reject `channel_id: 0` in the `channel/close` handler. - C-12: reject `channel_id: 0` in the `channel/close` handler.
- C-13: `channel/close` must drain (signal EOF to the handler, await - C-13: `channel/close` must drain (signal EOF to the handler, await
pump completion) before `abort`/decrement; implement REQ-CH-06 pump completion) before `abort`/decrement; implement REQ-CH-06
+31 -1
View File
@@ -7,6 +7,12 @@ ops-put task hits `channel:pub_open_not_implemented` when its Sink op
carrying the `channel_open` marker is invoked, exactly as the code carrying the `channel_open` marker is invoked, exactly as the code
comments predict. The downstream project is paused on this. comments predict. The downstream project is paused on this.
**All four findings resolved 2026-10-13** — implemented in alkcall
main ahead of the single 0.9.0 publish (see the resolution note under
S-01's remediation options; the pinned stub tests are deleted and the
`not_implemented` source-tree guard is part of the test suite,
`tests/stub_guard.rs`, wired into AGENTS.md's "Stub Guard" section).
Policy context: shipping stubs to crates.io is now a hard failure per Policy context: shipping stubs to crates.io is now a hard failure per
the repo owner's ruling. Any handler that registers successfully and the repo owner's ruling. Any handler that registers successfully and
returns an error of the form `*:not_implemented` on every invocation returns an error of the form `*:not_implemented` on every invocation
@@ -94,13 +100,37 @@ least fails loudly.
**Remediation options (pick one before the next release):** **Remediation options (pick one before the next release):**
> **CORRECTED 2026-10-13 (reviews 011 + ADR-047 amendment 4):** the
> implemented fix is option 1, but option 1's sketch below was the
> wrong shape for marked ops and is superseded — the correct shape is
> review 011's (also pinned as ADR-047 amendment 4): the initiator
> allocates and streams bytes on the channel before the open
> resolves; the responder adopts (`register_pub_openable`); the
> wrapper is the responder of record, awaiting the handler and
> merging ack fields + handler result into ONE response; zero
> `call.published` on marked ops (chunks rejected loudly);
> `RelayError::PubTypedOpen` is replaced by the relay put
> translation. Handing the `OpenHandler` the serving loop's
> `PublishStream` (the sketch below) is ADR-046's *unmarked* pub
> semantics and would have re-validated MB-scale binary payloads as
> JSON call chunks. The downstream agent caught this in review 011
> before the amendment was implemented.
> **RESOLVED 2026-10-13** — implemented in alkcall main (the single
> 0.9.0 publish, review 011 §6): S-01 via the marked-pub contract
> above, S-02/S-03 implemented (ADR-052/ADR-053), S-04
> forward-through (ADR-022, 2026-10-13 amendment), the
> `not_implemented` source-tree guard wired into AGENTS.md's
> "Stub Guard" section, and the pinned stub tests deleted.
1. **Implement the wrapper** — allocate the channel id, run the 1. **Implement the wrapper** — allocate the channel id, run the
establishment phase (reply fields carry the offer ack, per ADR-047 establishment phase (reply fields carry the offer ack, per ADR-047
§1 and ops-put's acceptance criteria), hand the `OpenHandler` the §1 and ops-put's acceptance criteria), hand the `OpenHandler` the
`PublishStream` already produced by the serving loop along with the `PublishStream` already produced by the serving loop along with the
channel's `Connection`. All plumbing exists; this is wrapper glue, channel's `Connection`. All plumbing exists; this is wrapper glue,
plus deciding the OpenHandler-reads-channel-vs-reads-PublishStream plus deciding the OpenHandler-reads-channel-vs-reads-PublishStream
question in an amended ADR-047. question in an amended ADR-047. *[Superseded sketch — see the
correction above; review 011's shape is the pinned one.]*
2. **Reject at registration** — return an error from 2. **Reject at registration** — return an error from
`register_openable_with_establisher` for `OperationType::Pub`, `register_openable_with_establisher` for `OperationType::Pub`,
mirroring `RelayError::PubTypedOpen` (src/channels/relay.rs:147-156), mirroring `RelayError::PubTypedOpen` (src/channels/relay.rs:147-156),
@@ -2,13 +2,22 @@
## Status ## Status
Open — filed 2026-10-13. From alkblobs (the downstream crate paused on Closed — filed 2026-10-13. From alkblobs (the downstream crate paused on
review 010 S-01; its ops-put task gates on this surface). Purpose: review 010 S-01; its ops-put task gates on this surface). Purpose:
correct review 010's S-01 remediation sketch before the amendment is correct review 010's S-01 remediation sketch before the amendment is
drafted, enumerate what the amendment must pin so one publish drafted, enumerate what the amendment must pin so one publish
(0.9.0) fixes all four stubs right, and record the downstream (0.9.0) fixes all four stubs right, and record the downstream
requirements and non-requirements from the consumer seat. requirements and non-requirements from the consumer seat.
**Closed 2026-10-13:** all seven §2 pins are implemented and pinned in
ADR-047 amendment 4 / ADR-052 / ADR-053 / ADR-022's 2026-10-13
amendment, with the §3 verification gates landed as tests
(`channels::operations::pub_open_tests`,
`channels::hub_leg::gate2_tests::gate_pub_relay_put_end_to_end`, the
registration-invariant tests in `registry::registration` and
`registry::op_register`), the guard is `tests/stub_guard.rs`, and the
docs rewrites below are done. The §6 sequencing concluded.
Input basis: alkblobs ADR-014 Input basis: alkblobs ADR-014
(`/workspace/@alkdev/alkblobs/docs/architecture/decisions/ (`/workspace/@alkdev/alkblobs/docs/architecture/decisions/
014-marked-pub-put-contract-and-alkcall-gate.md`, committed 7ac0099) 014-marked-pub-put-contract-and-alkcall-gate.md`, committed 7ac0099)
+2 -1
View File
@@ -132,7 +132,8 @@ impl ChannelRelay {
/// - `spec` must carry the `channel_open` marker (reconstructed /// - `spec` must carry the `channel_open` marker (reconstructed
/// WITH the marker by discovery, ADR-047 amendment 3). A /// WITH the marker by discovery, ADR-047 amendment 3). A
/// `Pub`-typed marked spec composes through the put-forwarding /// `Pub`-typed marked spec composes through the put-forwarding
/// translation (see [`Self::register_relay_pub_openable`]); an /// translation (`register_relay_pub_openable`; ADR-047 amendment 4
/// §4.7); an
/// unmarked spec is a /// unmarked spec is a
/// [`RelayError::UnmarkedSpec`] error — branch on the /// [`RelayError::UnmarkedSpec`] error — branch on the
/// marker before calling. /// marker before calling.