Files
alkcall/CHANGELOG.md
T
glm-5.3-flash a22b2b84c9 fix: park early-arrival chunks for un-adopted channels (open/first-data race)
The connect side adopts a channel (installs local routing state) only
after the open-op response arrives, but the accept side's OpenHandler
can start pumping data the moment the channel opens — the two race and
the demux's lenient unknown-channel drop (REQ-CH-04) silently lost the
producer's first chunks (a TTY backend's banner, a sub protocol's
greeting).

route_payload now parks up to 64 payloads per unknown channel_id in a
bounded early-arrival buffer; adopt_channel drains them into the new
receiver in order. Beyond the cap the chunk drops with the existing
debug log + dropped_unknown_chunks counter (which now also counts
overflow). clear_all drops parked buffers with the connection.

Surfaced by alktty's consumer end-to-end test (review #001 L3): the
session never resolved because the producer's first chunks (stdout
sentinel + exit chunk for an immediately-resolving backend) arrived
before the adopt and were dropped. REQ-CH-04 wording updated by this
behavior; ADR-039 §demux loop describes the lenient drop for genuinely
unknown channels, which remains the case past the cap.

Verification: cargo test 597 (2 rewritten for the new semantics +
route_payload_to_unknown_channel_parks_until_adopt gate);
--all-features 614; clippy -D warnings clean; fmt clean; doc 0
warnings; publish --dry-run ok; standalone probe (handler-writes-first
e2e over one connection) shows 0 dropped chunks with the fix vs 1
without.
2026-09-05 07:04:30 +00:00

18 KiB

Changelog

All notable changes to this crate are documented here. The format is based on Keep a Changelog, and this crate adheres to Semantic Versioning.

[0.4.1] - 2026-09-05

Bug-fix release: chunks arriving for a not-yet-adopted channel are parked instead of dropped (the open-op response / first-data race). No API changes.

Fixed

  • Early-arrival chunks for un-adopted channels are parked, not dropped. The connect side adopts a channel (installs local routing state) only after the open-op response arrives, but the accept side's OpenHandler can start pumping data the moment the channel opens — the two race. Previously the connect side's demux dropped those chunks (REQ-CH-04's lenient unknown-channel drop), silently losing the first chunks of any push-first producer (a TTY backend's banner or greeting, a sub protocol's initial frame). route_payload now parks up to 64 payloads per unknown channel_id in a bounded early-arrival buffer and adopt_channel drains them into the new receiver in order; chunks beyond the cap drop with the pre-existing debug log and dropped_unknown_chunks counter (the counter now means "early-arrival overflow or genuinely unknown channel", not just the latter). clear_all drops the parked buffers with the connection. Surfaced by alktty's consumer end-to-end test (review #001 L3): the session never resolved because the producer's first chunks (the stdout sentinel + exit chunk for an immediately-resolving backend) arrived before the adopt and were dropped.

[0.4.0] - 2026-09-05

input_schema is now enforced at call time (the advertise-vs-enforce leg ADR-016 promised: INVALID_INPUT is the registry's schema-mismatch error code). Minor bump — behavioral break for any caller that was passing schema-violating inputs to ops declaring a non-trivial input_schema and relying on the validation being documentation-only.

Changed

  • OperationSpec.input_schema is enforced by the registry on every dispatch. Until now the schema was advertise-only: services/schema disclosed it, the wire and gateway paths disclosed it, but no dispatch entry point consulted it (the only schema alkcall actually enforced was publish_schema per-chunk on Pub ops, P-03). The schema now compiles once at registration (same fail-closed rule as publish_schema/CF-003: an un-compilable schema is a registration error, never a silently-skipped contract) and is checked after the ACL gate in all three dispatch entry points — invoke, invoke_streaming, and invoke_sink (via resolve_sink_handler, preserving the P-08 single-source-of-truth property). Violations return CallError::invalid_input("input failed input_schema validation") with the input echoed in details. Raw-JSON-Schema semantics (permissive on unknown keys) — adapters that want closed-by-default enforcement keep doing their own hardening, as alkhttp's CompiledInputSchema already does; the two checks compose. Built-in ops are unaffected: alkcall's own specs declare the empty object schema (accepts anything) and services/schema already returned INVALID_INPUT for a missing name before its handler ran. Motivated by alktty's code review #001 (L1): the channels open-op wrapper hands the registry-checked input to the OpenHandler as the authoritative params, which requires the registry to actually validate it.
  • OperationRegistry gained an input_validator cache (compiled at registration, mirrored on fork and in OperationRegistryBuilder like the publish validators); input_validator() is pub(crate) — the public surface is unchanged.

0.3.1 - 2026-09-04

Bug-fix release: services/list-peers can now list peer-announced ops. No API breaks — OperationEnv gains one defaulted trait method (non-breaking for all implementors; cargo semver-checks 196 checks pass against the 0.3.0 baseline).

Fixed

  • services/list-peers shows each peer's operations (UP-03, surfaced by alkhttp's review 006). PeerCompositeEnv overrode peer_ids only, so peer_operations fell to the trait default (Vec::new()) and every peer listed with an empty operations array — the ADR-022 amendment's "announced op is discoverable via services/list-peers" promise never resolved on the wire. ADR-030 prescribed the fix but it had never been ported into alkcall; the existing list-peers unit tests passed because they mock peer_operations with hand-rolled envs. Implemented per ADR-030: OperationEnv::list_operation_names (default Vec::new())
    • overrides on OverlayOperationEnv (overlay map keys), PeerCompositeEnv (peer_operations delegates to the peer's overlay; its own aggregate mirrors contains(): session + connections + base), LocalOperationEnv (registry names), and ChannelsSessionEnv (delegates to base). Gate: announced_op_is_discoverable_via_services_list_peers exercises the exact compose_root_env shape (announce via op/register, then assert both the peer_operations probe and the services/list-peers wire shape attribute the op to the peer) — verified load-bearing (reverting the override fails the gate). ADR-030 status is now Accepted with the UP-03 provenance note.

0.3.0 - 2026-09-04

The connect side can serve (two-way ops over one channels connection), per-session fork registries, and the op/register bootstrap op — the remediation of reviews 004 and 005 (docs/reviews/004-*.md, docs/reviews/005-*.md). Two OperationRegistry methods changed their return types from borrowed to owned (source-breaking for annotated call sites; minor bump per 0.x semver rules) — verified with cargo semver-checks (196 checks pass against the published 0.2.0 baseline; the return-type changes were caught by manual diff review, as the tool has no lint for that pattern).

Added

  • OperationRegistry::fork — deep-copies a registry (handlers, provenance, composition authority, capabilities, cached publish-schema validators) into an independently-mutable copy. The per-session shape: fork the deployment's base registry, register the session's ops on the fork, dispatch the session over the fork (ADR-047 §4 amendment, 2026-09-03). Internally mutable, so a fork shared as an Arc can receive registrations after the dispatcher was built — install_bootstrap_discovery relies on this.
  • OperationRegistryBuilder::from_registry — seeds a builder from an existing registry's registrations (the fork surface expressed through the builder; registration order is not preserved — HashMap iteration).
  • Connect-side serving (two-way ops over one connection). The channels connect side's channel-0 read pump previously resolved outbound responses only and silently dropped inbound call.requested frames. New ChannelClient::from_connection_with_serving takes Option<ServingConfig> (registry + identity_provider); with Some(..) the read pump becomes the full-duplex serving loop (Dispatcher::serve_single_stream), so a connected peer can call the consumer's ops. from_connection keeps the resolution-only pump (pure-consumer default). Serving is opt-in: the protocol is symmetric, the API is explicit (ADR-022 amendment, 2026-09-03).
  • **Dispatcher::serve_single_stream and Dispatcher::dispatch_start
    • StartedDispatch** — the shared serving-loop machinery. Both single-stream loops (accept side and connect side) now run the same concurrency model: Once invocations, Sub pumps, and sink response writers are spawned, so same-connection nested composition resolves concurrently instead of deadlocking until the 30 s sweeper (review 005 G-01). Handles are tracked and aborted at loop exit; no lock is held across an await.
  • registry::op_register module — the op/register bootstrap op (review 004 F-05). A connected peer announces the ops it serves over the wire: OP_REGISTER_NAME (op/register), OpRegisterRequest (wire round-trip via to_json/from_json), op_register_spec / op_register_handler. Announcements land in the connection overlay and are served through forwarding handlers, so hub→consumer import (from_call) and consumer→hub announce are symmetric. Collision policy (review 005 G-03): a peer-announced op may replace other peer-announced ops (when replace is set) but never the serving side's own registrations — a name present on the serving registry rejects with the new CallError::already_exists (ALREADY_EXISTS, non-retryable), regardless of replace. The collision gate checks the serving registry, the session fork, and the connection overlay itself.
  • install_bootstrap_discovery (review 004 F-06) — registers services/list, services/list-peers, and services/schema against a registry with handlers closed over that same Arc, so per-session forks are the discovery source for their own openables (handlers see every op the registry serves at call time). ACL filtering stays per-caller. The bootstrap-op set on channel 0 is closed and now includes services/list-peers (review 005 G-05 — doc alignment; the code had installed it since the amendment landed) plus op/register (ADR-022 amendment).
  • spec_to_json_pub — public serialization of an OperationSpec into the services/schema wire shape (the shape op/register announces with and services/schema serves). resource_id_path now survives the spec wire round-trip (review 005 G-04): both the serializer and the op/register parser carry the field.
  • CallConnection::overlay_contains / CallConnection::overlay_registration — read access to the connection overlay, for op/register replace semantics and composition reachability checks.

Changed

  • OperationRegistry::registration returns Option<HandlerRegistration> (was Option<&HandlerRegistration>) and list_operations returns Vec<OperationSpec> (was Vec<&OperationSpec>). Source-breaking for call sites that annotate the borrowed types — clone-at-the-boundary instead. Minor bump per 0.x semver rules.
  • Registration is &self throughout — OperationRegistry::register, ChannelOperations::register_on, and ChannelCore::register_openable take &OperationRegistry (was &mut). Source-compatible for callers (reborrow); this is what makes post-construction registration on a forked, Arc-shared registry possible.
  • from_call composition reachability — the forwarding-handler path declares reachable namespaces on the composing handler's scoped_env (empty is deny-by-default) and the connection overlay is attached only for identity-carrying connections (ADR-030 §5).

0.2.0 - 2026-08-31

Consumer-findings remediation (CF-001..004 from docs/reviews/consumer-findings-ledger.md — alkhttp as the first real consumer), the promoted gateway dispatch spine (ADR-048), and a docs-hygiene pass. One behavior change noted below; otherwise additive.

Added

  • gateway feature: the transport-neutral dispatch spine (ADR-048). New alkcall::gateway module behind the opt-in gateway cargo feature (default off; adds no dependencies). GatewayDispatch is the deadline-bounded, re-rooted-context invoke spine over OperationRegistry (invoke / invoke_streaming / invoke_sink) promoted from alkhttp's gateway after it proved transport-agnostic — hubs and spokes relaying calls (ADR-042 translate path) need the identical root-context discipline (internal: false, forwarded_for: None) without any HTTP. schema_disclosure_denial is the shared is-internal + ACL check for services/schema inner-op-name disclosure (one implementation so transports cannot drift; ACL denial returns FORBIDDEN, Internal visibility returns spec-404 — see ADR-048 for the split from the wire handler's conservative spec-404). The handler deadline is a constructor knob (with_deadline); the default remains 30 s. alkhttp migrates to this module in a follow-up and drops its local copy.

Changed

  • Un-compilable publish_schema values are rejected at registration time (CF-003). OperationRegistry::register and every OperationRegistryBuilder method now return Err when a Pub op's publish_schema fails to compile. Previously the bad schema was accepted and the dispatch path failed open (warn log, chunks unvalidated). The compiled validator is now cached per-op (OperationRegistry::publish_validator) and consumed by the dispatch path — the per-request compile is gone. Code that registered un-compilable schemas will now get a registration error instead of a silently-unvalidated op.

Fixed

  • services/schema no longer discloses Internal or ACL-restricted op specs (CF-004). The schema handler now applies the same gates as invoke() — Internal-visibility rejection and AccessControl::check with matching identity resolution. Restricted ops return spec-404 (NOT_FOUND), consistent with "restricted ops don't exist" elsewhere; no information leaks about the restricted surface. The gate is inside the handler, so every transport is covered.
  • Demux TooLarge skip no longer allocates from the peer's header (CF-002). The skip now streams through a fixed 64 KiB buffer, and a cumulative 256 MiB skipped-bytes budget tears down connections that loop oversized headers. Previously the buffer was sized from the untrusted u32 length (up to ~4 GiB pinned per dribbling peer).
  • Write failures before request delivery are retryable (CF-001). New CallError::connection_closed (CONNECTION_CLOSED, retryable: true) is returned when the call.requested frame write fails on any consumer path (call/subscribe/publish, both stream modes) — the call provably never reached the producer, so reconnect/retry is safe. Mid-publish write failures and producer-side connection-teardown failures remain non-retryable INTERNAL (delivery ambiguous). The new code string is additive; the retryable flag is the machine-readable signal for consumers.

Fixed (continued)

  • Doc hygiene. Resolved the broken intra-doc link on lib.rs's feature-gated gateway mention (rustdoc warned on default-feature builds), and re-pointed 40 src/ doc references from the old alknet mono-repo ADR numbering (ADR-049/050/052/065/070/074/092) to this crate's numbering (ADR-021/011/034/007/008/009/005) so published docs reference ADRs this crate actually carries. Failure-path coverage was added alongside: the CF-002 skipped-bytes teardown and skip-EOF arms, CF-001's subscribe write-failure sites (both stream modes), the publish request-frame failure in single-stream mode, and non-retryability pinning for mid-publish failures.

0.1.1 - 2026-08-17

A minor release that renames the ALPN prefix from alknet/ to alk/ and drops the alktype dependency. No public API changes — verified with cargo semver-checks (196 checks pass, no semver update required).

Changed

  • ALPN prefix renamed. All ALPN strings changed from alknet/<name> to alk/<name>: alknet/call → alk/call, alknet/channels → alk/channels. The rename landed before the first published consumer (alktty), so no interop break exists. CHANNELS_ALPN is now b"alk/channels"; CallAdapter::alpn() returns b"alk/call"; derive_alpn_from_op_name derives alk/<alpn> from channels/<alpn>/(sub|pub) op names. ADR-004 amended with the rename rationale. See Review 003.
  • alktype dependency dropped. The only usage (alktype::validation::build_validator) was a thin wrapper over jsonschema::options().build(); the call site now uses jsonschema directly. The crate is leaner — no alktype (and no transitive indexmap/hashbrown 0.17) in the dependency tree.
  • tokio features slimmed. features = ["full"] replaced with ["rt", "sync", "time", "io-util", "macros"] — the crate uses no net/fs/process features. This unblocks wasm32-unknown-unknown.
  • WASM support. The crate now compiles for wasm32-unknown-unknown (verified with cargo check and cargo clippy on the wasm target). uuid gained the rng-getrandom feature and getrandom 0.4 the wasm_js feature so request-ID generation works on wasm.

Added

  • BAST document for the chunk header. docs/architecture/chunk-header.bast.json is the machine-readable wire-format spec for the channels 8-byte chunk header ([channel_id: u32 BE][length: u32 BE]). BAST is plain JSON — consumable by any language; the alktype crate compiles it into readers/writers/validators, and future codegen derives language-specific implementations. Embedded in the crate as channels::wire::CHUNK_HEADER_BAST so downstream Rust crates can consume it without a file lookup. The hand-rolled parse_header/write_header functions remain the hot path; the BAST document is the contract.

0.1.0 - 2026-08-14

Initial crates.io release. The unification of alknet-call (structured JSON RPC: operations, streaming subscriptions, service discovery) and alknet-channels (multiplexing proxy: N logical channels over one transport stream, channel 0 pre-negotiated as the call protocol). Vendored core types (Connection, ProtocolHandler, BiStream, BidiStreamSource, AuthContext, IdentityProvider, Capabilities, OwnershipProvider), the Pub operation type / HandlerKind::Sink (ADR-046), the channels protocol with openable-ALPNs-as-operations (ADR-047), and the ChannelClient transport-agnostic client.