Commit Graph
17 Commits
Author SHA1 Message Date
glm-5.3-flash 944c300c17 feat(channels): implement the single-publish fixes — S-01..S-04 (reviews 010/011; ADR-047 am. 4, ADR-052, ADR-053, ADR-022 am.)
S-01 — the marked-pub open contract (ADR-047 amendment 4):
- ChannelCore::register_pub_openable + PubOpenHandler (the put path's
  handler returns its result for the merged single response; the
  wrapper awaits inline, teardown is EOF-driven, handler_task stays
  None); register_openable* refuses Pub-typed specs; the wrapper entry
  points refuse unmarked/non-Pub shapes
- the reserved input key `channel_id` (the initiator-allocated id):
  stripped pre-validation by resolve_sink_handler (the single source of
  truth, P-08), handed to the wrapper via OperationContext.metadata, a
  missing key rejected INVALID_INPUT; adopt parity enforced
  (ManagerError::InvalidAdopt) and adopt failures answer
  channel:open_failed / adopt_failed
- zero call.published on marked ops: the first publish chunk on a
  marked op is rejected loudly (channel:publish_chunks_rejected) in
  both dispatch loops; the handler is never fed
- relay put-translation replaces RelayError::PubTypedOpen: hub
  allocates odd on the producer leg, opens the spoke op in the
  background (put-acks-in-result), one-direction pump, response-mirror
  with the producer's reserved key stripped
- ChannelClient::open_pub_channel + PubOpenReply (the initiator-
  allocates put open capability)

S-02 — channel/control implemented (ADR-052): establisher-contributed
ChannelControl (Establishment::with_control), manager-owned registry,
teardown removals, channel:no_control_handle capability answer.

S-03 — channel/resources/subscribe implemented (ADR-053): the
ResourceEnumerators registry (shared per peer), snapshot-then-changes
enumerators, scan-once aggregation, the empty seed; the
always-failing one-shot error stream removed.

S-04 — op/register forward-through (ADR-022 amendment): announced
unmarked Sub/Pub register the real streaming/sink forwarding shapes;
marked announcements refused with MARKED_SPEC_UNSUPPORTED; marked
specs refuse the generic registration path and register_imported
(Result-returning now).

Guard (review 010 action 3): tests/stub_guard.rs greps src/ for
not_implemented; the four pinned stub tests are deleted and the
marked-spec registration refusals are pinned by tests.

Verified: cargo test (696 + guard), clippy --all-targets -D warnings,
fmt --check, fuzz corpus replay.
2026-10-13 09:00:21 +00:00
glm-5.3-flash 1e20bb77d0 chore: prepublish review — bump to 0.3.0, changelog, doc alignment
0.2.0 is already on crates.io (2026-08-31, c16b069); the review
004/005 remediation work is unreleased on top of it and lands as
0.3.0.

- Bump version 0.2.0 -> 0.3.0. Two OperationRegistry methods changed
  borrowed returns to owned (registration, list_operations) —
  source-breaking for annotated call sites, minor bump per 0.x
  semver rules. cargo semver-checks passes (196 checks) against the
  published baseline; the return-type changes were caught by manual
  diff review.
- CHANGELOG 0.3.0: connect-side serving (from_connection_with_serving
  + ServingConfig), OperationRegistry::fork + builder from_registry,
  registry::op_register (bootstrap op, collision policy,
  ALREADY_EXISTS), install_bootstrap_discovery, spec_to_json_pub +
  resource_id_path round-trip, overlay accessors, concurrent serving
  loops, &self registration.
- README: serving-as-consumer section, consumer role table update,
  drop the stale `mut` on the registry example.
- AGENTS.md: ADR range 001..047 -> 001..048.
- Fix rustdoc private-intra-doc-link warning on StartedDispatch.

Verification: 589 default / 606 all-features tests, clippy
(all-targets, all-features, wasm32) clean, fmt clean, doc clean,
publish dry-run OK.
2026-09-04 14:14:22 +00:00
glm-5.3-flash 1cbb7c6536 fix(review 005 Unit 1): concurrent serving loops + stub-exercising gates (G-01, G-02)
- Split dispatch() into dispatch_start() (sync prefix) + spawned
  invocation: both single-stream loops (serve_single_stream and the
  accept-side run_loop_single_stream) spawn Once invocations, Sub
  pumps, and sink response writers; only the Pub sink start stays
  inline (chunk_tx must register before the next call.published).
  Inline dispatch deadlocked same-connection nested composition: the
  read loop awaited the parent handler, which awaited a nested call
  whose response only the same read loop could resolve (resolved only
  via the 30s sweeper). Spawned handles tracked + aborted at loop exit;
  in_flight_sinks behind an Arc<parking_lot::Mutex> with guards dropped
  before awaits.
- run_loop_single_stream gains the pending-resolution arms
  (RESPONDED/COMPLETED/ERROR): the accept side previously served only
  and had no loop resolving its own outbound pendings in single-stream
  mode — the latent accept-side imported-op composition hazard is
  mechanized shut.
- Write-failure in the spawned Once path warns instead of closing the
  loop (matches the Sink arm; dying transport still surfaces via
  ConnectionClosed on the next read).
- G-02 gate: hub_handler_composes_peer_announced_op_via_nested_composition
  — announce -> consumer calls hub/compose -> hub's serving loop
  wire-dispatches it -> handler composes via ctx.env -> forwarding
  stub's nested call crosses back to the consumer. The F-05 gate
  bypassed this path entirely.
- Interleaved-directions gate: outbound_call_resolves_while_inbound_
  subscription_is_being_served — consumer serves a live Sub while a
  wire-dispatched hub handler issues an outbound call on the same
  connection.
- Both gates verified load-bearing: run against the pre-fix loop each
  reproduces the G-01 hang (no progress, bounded-timeout failure);
  post-fix both resolve in <0.2s, no sweeper evictions.

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

Refs docs/reviews/005-...md (G-01, G-02; Units 2-3 open).
2026-09-04 09:36:08 +00:00
glm-5.3-flash f84d214173 feat: per-session fork registry, connect-side serving loop, op/register (review 004 Units 1-3)
Remediates all six findings of review 004 (per-connection dispatch
resolution and client-side op serving). All claims re-verified in
source before remediation; F-02's member list gains ScopedPeerEnv
(also Clone — fork surface simpler than estimated).

- OperationRegistry: interior mutability (parking_lot RwLock on both
  maps); register takes &self; registration/list_operations return
  owned clones; fork() deep-copies registrations + cached publish-schema
  validators (F-02/F-03); OperationRegistryBuilder::from_registry.
- install_bootstrap_discovery: services/list, services/list-peers,
  services/schema registered closed over the fork itself, so
  per-session openables are discoverable and services/schema answers
  from the fork (F-06).
- Dispatcher::serve_single_stream: full-duplex single-stream loop —
  call.requested dispatches inbound; responded/completed/error resolve
  outbound pendings; aborted tries both tables (in-flight sink aborts
  + pending cascade); published routes inbound sinks (F-04).
- ChannelClient::from_connection_with_serving(connection,
  Option<ServingConfig>): opt-in serving; from_connection keeps the
  pure-consumer default.
- registry::op_register: OpRegisterRequest wire DTO (spec in
  services/schema JSON + replace flag), op_register_spec,
  op_register_handler (rebuild -> forwarding stub -> register_imported,
  forced Internal/FromCall), announce_op; CallError::already_exists;
  spec_to_json_pub; from_call's rebuild_spec_for + forwarding-handler
  constructors crate-shared (F-05).
- ADR-047 §4 amendment #2: per-session fork is the dispatch-registry
  mechanism; overlay stays nested-invocation/peer-announced landing
  zone (F-01/F-02).
- ADR-022 amendment 2026-09-03: bootstrap-op set (services/list,
  services/schema, op/register), opt-in connect-side serving, op/register
  wire shape (F-04/F-05).
- alkhttp ADR-048 reconciliation note + OQ-05 re-pointed at the alkcall
  ADRs (Unit 1b).
- Review 004 status -> remediated; remediation log with gates.

Verification:
- cargo test: 581 passed, 0 failed (565 baseline + 16 new)
- 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 clippy --target wasm32-unknown-unknown -- -D warnings: clean
- cargo fmt --check: clean
- cargo doc --no-deps: clean

Gates: fork_registry_open_op_resolves_and_is_discoverable (open op via
fork + services/list shows openable + services/schema validates),
serving_loop_hub_to_consumer_call_resolves (hub->consumer call through
consumer's serving loop, consumer->hub still resolves),
op_register_announce_then_hub_call_routes_back_to_consumer (announce ->
overlay -> hub call -> forwarding stub -> consumer serves).
2026-09-03 17:32:45 +00:00
glm-5.3-flash ae372c0c7e test+docs: prepublish hardening from review (failure paths, doc hygiene)
Coverage:
- CF-002: demux skipped-bytes budget teardown test (268 MiB skip in-memory;
  a budgetless demux wedges in the 17th skip, the real one tears down) and
  the skip-hits-EOF arm (truncated oversized payload ends the loop).
- CF-001: retryable CONNECTION_CLOSED pinned for subscribe write failures
  in both stream modes and single-stream publish request-frame failures;
  non-retryable INTERNAL pinned for mid-publish failures in both modes
  (deterministic FailOnFlushN write half).
- Gateway: invoke_sink with with_deadline(None) completes a slow sink.

Docs:
- Fix broken intra-doc link on lib.rs's feature-gated gateway mention
  (rustdoc warned on default-feature builds).
- Re-point 40 src/ references from the old alknet mono-repo ADR numbering
  (049/050/052/065/070/074/092) to this crate's numbering
  (021/011/034/007/008/009/005); drop into_sub_streams references
  removed by ADR-035.

Verification: 565 default / 582 all-features (8 new), clippy -D warnings
on default/gateway/all-features/wasm32, fmt clean, rustdoc warning-free
on default and all-features, publish dry-run clean. Consumer-facing API
continuity 0.1.1 -> 0.2.0 verified by compiling an API-surface probe
against both versions.
2026-08-31 09:56:07 +00:00
glm-5.3-flash 8cb2a6eb6d fix: remediate consumer findings CF-001..004 (alkhttp ledger)
- CF-004: services_schema_handler now applies the same visibility +
  AccessControl gates as invoke() (identity resolution mirrors invoke:
  handler_identity under internal). Restricted ops return spec-404 NOT_FOUND
  — matches "restricted ops don't exist" and leaks nothing about the
  restricted surface. Closes the unauthenticated /call-path disclosure.
- CF-003: publish_schema compiled at registration time (both
  OperationRegistry::register and OperationRegistryBuilder::store);
  un-compilable schemas are a registration error — an unvalidated ingest
  path can no longer be constructed. Compiled validator cached per-op
  (publish_validator) and consumed by dispatch; per-request compile gone.
  BEHAVIOR CHANGE: register/builder reject un-compilable publish_schema.
- CF-002: demux TooLarge skip streams through a fixed 64 KiB buffer
  instead of allocating the peer-declared length (u32, up to ~4 GiB);
  cumulative 256 MiB skipped-bytes budget tears down dribbling peers.
  Existing resync test passes unchanged.
- CF-001: new retryable CallError::connection_closed (CONNECTION_CLOSED)
  applied only where the call is provably undelivered — request-frame
  write failures on all consumer paths (call/subscribe/publish, both
  stream modes; publish pump tags write stages). Mid-publish failures and
  producer-side fail_all stay non-retryable INTERNAL (delivery ambiguous).
  New code string is additive; retryable flag is the machine-readable
  signal.

Verification: cargo test (558 pass, 15 new), clippy --all-targets -D
warnings, fmt --check, wasm32-unknown-unknown check.
2026-08-31 08:22:55 +00:00
glm-5.2 44d4b496e8 refactor: drop alktype dependency; add BAST doc for chunk header
- Remove alktype from Cargo.toml (its only usage was a thin wrapper
  over jsonschema::options().build())
- Replace alktype::validation::build_validator with direct jsonschema
  in dispatch.rs
- Add docs/architecture/chunk-header.bast.json — the chunk header's
  BAST (Binary Abstract Syntax Tree) machine-readable wire spec
- Embed as channels::wire::CHUNK_HEADER_BAST via include_str! so
  downstream Rust crates can consume it without a file lookup
- Add test asserting the embedded BAST doc is valid JSON and matches
  the wire format
- Update AGENTS.md §10 and implementation-specialist.md: BAST docs
  are the contract; trivial/hot-path formats stay hand-rolled,
  complex formats use the alktype engine or codegen

Verification:
- cargo test: 543 passed, 0 failed
- cargo clippy --all-targets -- -D warnings: clean
- cargo fmt --check: clean
- cargo doc --no-deps: clean
- BAST doc compiles + round-trips against alktype v0.2.0 engine
2026-08-17 06:08:13 +00:00
deepseek-v4-pro 08e7df2aa0 feat: rename ALPN prefix from alknet/ to alk/ (v0.1.1)
- CHANNELS_ALPN: b"alknet/channels" → b"alk/channels"
- CallAdapter::alpn(): b"alknet/call" → b"alk/call"
- derive_alpn_from_op_name: alknet/ prefix → alk/ prefix
- All ALPN string literals in src/ and docs/ updated
- ADR-004 amended with prefix rename rationale
- AGENTS.md, README.md updated
- Version bumped to 0.1.1

Review: docs/reviews/003-alpn-prefix-rename.md

Verification:
- cargo test: 542 passed, 0 failed
- cargo clippy --all-targets -- -D warnings: clean
- cargo fmt --check: clean
- cargo doc --no-deps: clean
2026-08-14 13:55:28 +00:00
deepseek-v4-pro 49e01c1ff4 fix: Unit 9 — abort-cancels-Pub (P-06)
- Add in_flight_sink_aborts map to Dispatcher for cross-stream abort
  signaling via oneshot channels shared across handle_stream tasks
- handle_abort signals in-flight sink aborts before cascading to
  PendingRequestMap
- pump_sink registers an abort receiver, selects on it alongside the
  handler and reader; on abort (same-stream or cross-stream), drops
  the handler future instead of awaiting it — no response written
- InFlightSink gains handler_handle: JoinHandle<()>; single-stream
  EVENT_ABORTED aborts the handler task via JoinHandle::abort()
- 4 new tests: same-stream abort drops handler, cross-stream abort
  drops handler, cleanup of in_flight_sink_aborts map, handler
  resource release via DropGuard

Verification:
- cargo test: 483 passed, 0 failed
- cargo clippy --all-targets -- -D warnings: clean
- cargo fmt --check: clean
- cargo doc --no-deps: clean
2026-08-14 06:21:58 +00:00
glm-5.2 f362c9e596 fix: Unit 8 — Pub protocol semantics (P-03, P-04, P-07)
The publish path's decided-but-unimplemented behaviors land: per-chunk
schema validation, initiator call.error terminates the stream, and an
early handler return short-circuits the feed. alktype::validation::
build_validator is now used on both dispatch paths (the dependency was
declared and unused); jsonschema was added as a direct dependency
because its Validator type is part of alktype's public API.

P-03 — OperationSpec::publish_schema was declared, had a builder,
defaulted to None, and was never read. It now round-trips through
discovery: spec_to_json emits "publish_schema" when Some,
operation_spec_schema() advertises the property, and rebuild_spec_for
parses it back (null/absent treated as None, so an imported Pub op no
longer loses the schema). Both dispatch paths (pump_sink and
run_loop_single_stream) validate each call.published chunk's input
against the schema before yielding it to the SinkHandler; on
validation failure an Err(CallError::invalid_input(...)) with the
offending chunk in `details` is injected and the feed terminates,
matching the SinkHandler contract that an Err terminates the stream.
The validator is built once at dispatch time and carried in the
SinkDispatch (and, for the single-stream path, in a new InFlightSink
struct alongside chunk_tx); a schema that fails to compile logs a
warn and falls back to no validation. When publish_schema is None,
chunks are yielded as-is (unchanged behavior).

P-04 — pump_sink matched only call.published/call.completed/call.
aborted and dropped everything else into a debug-log ignore branch; a
call.error from the initiator was silently ignored. Both dispatch
paths now match call.error, parse the CallError from the payload,
inject it as Err(call_error) into the sink's chunk_tx, and terminate
the feed — the handler sees the initiator's error, not a synthetic
"aborted" message. A malformed payload falls back to
CallError::internal("publish error from initiator (malformed)").

P-07 — pump_sink used tokio::join!(feed_fut, handler) and only wrote
the response after both completed; an early-returning handler (e.g.
rejects after chunk 1) had its response deferred until the initiator
finished publishing or the next chunk_tx.send failed. Replaced with a
tokio::select! loop over handler.fuse() and reader.read_frame(): when
the handler completes first, the response is captured and the loop
breaks immediately, the feed is short-circuited (chunk_tx dropped,
the handler's PublishStream sees EOF), and the response is written
without waiting for the feed. The feed-wins branch (natural end /
abort / error / read failure) awaits the handler after the loop as
before. This removes the unbounded stall on a path ADR-046 intends
for long-lived streams.

Tests (15 new, 480 total):
- discovery: spec_to_json emits/omits publish_schema; operation_spec_
  schema documents the property (3).
- from_call: rebuild_spec_for parses publish_schema (present/omitted/
  null) and round-trips with spec_to_json (4).
- dispatch (stream-per-request): publish_schema rejects invalid chunk
  + passes valid chunks + no-schema yields as-is (3); initiator
  call.error terminates with the initiator's error + malformed
  fallback (2); early handler return short-circuits a slow feed and
  the response is not deferred (1).
- channels/client (single-stream): publish_schema rejects invalid
  chunk over channel 0; initiator call.error terminates the publish
  over channel 0 (drives run_loop_single_stream directly with crafted
  frames since the public publish() API takes Stream<Item = Value> and
  cannot emit an initiator call.error) (2).

Verification:
- cargo test              → 480 passed, 0 failed (was 465; +15 new tests)
- cargo clippy --all-targets -- -D warnings → clean
- cargo fmt --check       → clean
- cargo doc --no-deps     → 0 warnings
2026-08-13 09:43:39 +00:00
glm-5.2 da12d65a03 fix: Unit 7 — small correctness fixes (C-19-misc, C-21, C-23, P-13)
No panic paths in library code, no misleading public fields, honest
error envelopes, latent bugs in derive_alpn_from_op_name and the mux
pump map fixed, per-discovery Box::leak removed, demux observability
counter added, resources-snapshot unbounded loop replaced with an
O(open channels) iterator.

C-21 — write_header (wire.rs) sliced out[..CHUNK_HEADER_LEN] and
panicked on short buffers; the doc comment declared the panic. Returns
Result<_, ChunkError::HeaderTooShort> instead (the variant already
existed for the parse side). write_chunk and all callers updated (they
pass [0u8; 8] today so the Result is always Ok, but the API contract
is typed, not panic-documented). Added a short-buffer test.

C-23 — dispatch_requested's Sink and Stream error arms used
String::new() as the request id (dispatch.rs), producing envelopes
with an empty id. The request_id is now cloned before the move into
dispatch and used in the error arms, so the envelope carries the real
id (matching the Once arm, which already did this correctly).

P-13 — SinkDispatch::chunk_tx was pub (dispatch.rs), exposing the
futures::mpsc::Sender type and the 64-slot buffer size as effective
public API. Made pub(crate); handle_stream and pump_sink (the only
readers) are in the same module, and the dispatch tests are in the
same module too.

C-19 (mux pump map leak + duplicate registration) — MuxRunner::run
inserted each pump's JoinHandle into self.pumps and never removed
finished ones (they accumulated for the connection's lifetime).
Duplicate register(channel_id) silently overwrote the old handle while
the old pump kept running (two pumps, one channel id). Fixed: before
insert, check for an existing entry; if it is_finished(), reap it and
proceed (leak fix); if it is still running, reject the registration by
dropping the responder without sending (the caller's receiver.await
yields RecvError, which ChannelManager maps to ChannelExists). Added
two tests: duplicate-rejected-while-running and
reap-finished-then-reregister.

C-19 (derive_alpn_from_op_name) — from_call.rs took only the first
path segment (rest.split('/').next()), so channels/custom/proto/sub
derived alknet/custom instead of the full ALPN custom/proto. The
segment.starts_with("alknet/") branch was dead (a single segment
cannot contain /), and segment == "alknet" yielded the nonsense ALPN
"alknet". Fixed: strip the known /sub or /pub suffix from rest
instead of taking the first segment, so multi-segment ALPNs survive.
The rule (ADR-047 Negative): alknet/*-prefixed segments and
multi-segment non-alknet/* ALPNs are returned as-is; single-segment
non-alknet names get the alknet/ prefix prepended. Added tests for the
multi-segment non-alknet case, the explicit alknet/ prefix case, the
/pub suffix, and the no-suffix (non-channel-open-op) case.

C-19 (Box::leak per discovery) — from_call.rs leaked a String to
'static on every leak_alpn call (bounded per unique ALPN in theory,
but leaks on every rediscovery of every marked op). Refactored
ChannelOpenSpec::alpn from &'static str to Cow<'static, str>
(spec.rs); the common case (ALPN crates register at compile time with a
&'static str literal) pays no allocation via Into<Cow>, while from_call
supplies an owned String without leaking. Removed leak_alpn. Amended
ADR-047 §2 to record the Cow<'static, str> shape (two-way-door type
detail; the wire format — a boolean channel_open marker — is
unchanged).

C-19 (REQ-CH-04 error counter) — the demux's lenient unknown-channel
drop (manager.rs) only debug!-logged; there was no counter or stats
surface. Added an AtomicU64 dropped_unknown_chunks counter to
ChannelManager, incremented on the unknown-channel drop in
route_payload, with a dropped_unknown_chunks() accessor for
observability. Added two tests: counter increments per drop, and
known-channel routing does not increment it.

C-19 (resources-snapshot unbounded loop) — operations.rs iterated
0..u32::MAX with a per-iteration mutex lock, breaking when
resources.len() >= manager.open_count(). With sparse ids (monotonic
after churn) or a channel closing mid-iteration, this could iterate
millions of times. Replaced with an iterator over ChannelManager::
channel_ids() — a new accessor that returns a point-in-time Vec<u32>
of open channel ids — so the snapshot is O(open channels). Added
channel_ids tests and behavioral tests for the resources/subscribe
handler (open channels emit their ALPNs; no channels emit an empty
set). Also added close/control handler tests (C-25 #8 coverage the
review noted was missing): close rejects channel 0, close on unknown
channel returns NOT_FOUND, control on unknown channel returns
NOT_FOUND, control missing channel_id returns INVALID_INPUT.

Verification:
- cargo test              → 465 passed, 0 failed (was 449; +16 new tests)
- cargo clippy --all-targets -- -D warnings → clean
- cargo fmt --check       → clean
- cargo doc --no-deps     → 0 warnings
2026-08-13 09:15:20 +00:00
glm-5.2 bfb265e31b fix: Unit 6 — convention + doc cleanup (C-09, C-20-rem, C-22-rem, C-24, P-10, P-11)
Conventions satisfied, `cargo doc` clean, no actively-wrong comments,
producer/consumer naming consistent in the call/registry docs.

P-10 — `pump_sink` matched the string literals "call.published",
"call.completed", "call.aborted" (dispatch.rs) instead of the
EVENT_PUBLISHED / EVENT_COMPLETED / EVENT_ABORTED constants the rest
of the file imports. Replaced with the constants — pure refactor
hazard, no behavior change.

C-20 remainder — the wrong "SAFETY:" comment at from_call.rs:271
marked no `unsafe` block and was factually wrong (described a `'static`
return that isn't what `derive_alpn_from_op_name` does — it returns
`Option<String>`; the leak happens in `leak_alpn`). Reworded to a
plain note about the `'static` lifetime requirement. Also reworded
the abort-cancels claims in the `pump_sink` and `pump_stream` doc
comments (dispatch.rs): both claimed `call.aborted` "cancels the task
and drops the handler future" — the handler is actually `join!`-ed to
completion and not yet cancelled (the abort-cancels-Pub mechanism is
review 001 Unit 9). Trimmed step-numbered narration comments in
adapter.rs / client.rs that restated what the code does, keeping the
ordering-constraint and REQ-CH comments. The big reassembly.rs
deliberation landed with Unit 4; this finishes the remainder.

C-09 — fixed the 2 remaining `cargo doc` warnings (was 4; the
register_openable links were fixed in Unit 3):
- `unresolved link to default_policy` (operations.rs:50) — the
  [`default_policy`] intra-doc link resolves to
  super::policy::default_policy; used the full path.
- `env is both a module and a macro` (channels/mod.rs:30) — the
  [`env`] link collided with the std `env!` macro; qualified as
  [`self::env`].
`cargo doc --no-deps` now emits 0 warnings.

C-22 remainder — removed the filler `PhantomData` test at client.rs
(`let _ = std::marker::PhantomData::<ChannelClient>;` — asserts
nothing). The env.rs tautology was already removed in Unit 3.

C-24 — replaced "client→server streaming" with "producer→consumer
streaming" in call-protocol.md, operation-registry.md, README.md, and
open-questions.md (4 occurrences). Per AGENTS.md §8 the convention is
producer/consumer, not server/client. The remaining "client→server"
references in channels ADRs 034/037 are in stream_type table contexts
that Unit 10 (C-26) will handle as part of the spec-doc renumbering.

P-11 — amended ADR-046 §3's SinkHandler type so the stream item type
matches §6. §3 declared `Pin<Box<dyn Stream<Item = Value> + Send>>`;
§6 declared `Pin<Box<dyn Stream<Item = Result<Value, CallError>> +
Send>>`. The code uses §6's shape uniformly (registration.rs:32-40,
aliased as PublishStream). §3's text and the Door-type section are
amended to match §6; the Door-type section already marked the concrete
stream item type a two-way-door detail, so this is a text correction,
not a design change. Added an amendment note dated 2026-08-13.

Verification:
- cargo test --lib         → 449 passed, 0 failed (was 450; -1 removed filler test)
- cargo clippy --all-targets -- -D warnings → clean
- cargo fmt --check        → clean
- cargo doc --no-deps      → 0 warnings (was 2)
2026-08-13 08:47:20 +00:00
glm-5.2 495e04ed43 fix: Unit 2 — channel 0 single-stream call mode (C-01, C-25 #1)
Channel 0 was dead in both directions (C-01): the call protocol's
stream-per-request model (open_bi per call) is incompatible with
channel 0's single yield-once BiStream — every call_open_op failed
with StreamClosed on the connect side, and channel 0's Connection was
a black hole on the accept side.

The fix is single-stream call mode (ADR-036 amendment): all
EventEnvelope frames are multiplexed on channel 0's one BiStream.

Changes:
- CallConnection gains single_stream_writer: Option<Arc<SharedFrameWriter>>
  and new_single_stream() constructor. call_with_payload,
  subscribe_with_payload, publish_with_payload, and abort branch on
  is_single_stream() — in single-stream mode they write frames through
  the shared writer (mutex-serialized) instead of opening a fresh
  open_bi per call.
- Dispatcher::run_loop_single_stream reads frames off channel 0's read
  half, dispatches call.requested, writes responses through the shared
  writer, and routes in-flight call.published/call.completed/
  call.aborted to the matching Pub sink's chunk_tx by request_id.
- ChannelsAdapter::handle's InstallChannelZero hook now receives
  channel 0's Connection (built by the adapter) and runs the
  single-stream dispatch loop on it — closing the accept-side black
  hole. Mux runner is spawned BEFORE install_channel_zero so
  mux.register(0) can complete.
- ChannelClient::from_connection uses CallConnection::new_single_stream
  and spawns a read pump (read_single_stream_until_closed) that routes
  channel-0 response frames into the PendingRequestMap via
  dispatch_envelope — closing the connect-side StreamClosed path.
- ADR-036 amendment records the single-stream-mode decision (two-way
  door: implementation detail, wire format unchanged).

Acceptance gate (C-25 #1): three end-to-end tests wire
ChannelClient ↔ ChannelsAdapter over a real tokio::io::duplex pair
carrying the channels 8-byte chunk header wire format:
- channel_0_end_to_end_call_round_trip: a Query op round-trip
- channel_0_end_to_end_unknown_op_returns_not_found: NOT_FOUND
- channel_0_end_to_end_publish_delivers_chunks: a Pub op with 3 chunks

Verification: 437 tests pass (was 434; +3 e2e), clippy clean, fmt
clean, doc warnings unchanged (4, pre-existing C-09).
2026-08-12 21:55:04 +00:00
glm-5.2 502488cc72 fix: Unit 1 — make publish() work end-to-end (P-01, P-02, P-05, P-08, P-09, P-12 #1)
Fixes the Pub operation's client→responder path so a publish() actually
works on any transport. Previously the wire was broken in three
compounding ways and the unit tests couldn't see it.

P-01 [critical] — publish chunks were written to a *fresh* open_bi
stream (stream B) while the responder read chunks from the stream that
carried call.requested (stream A). Stream B's frames were silently
discarded by the dispatch loop's "ignoring non-requested event" branch.
On single-stream transports (TCP+TLS, SSH) the second open_bi returns
StreamClosed outright, so every publish failed. Fix: pump call.requested
+ call.published + call.completed on the *same* write half via the new
pump_publish_to_wire free function; the read half is pumped concurrently
for the single call.responded.

P-02 [major] — publish() / publish_with_payload() now take
Pin<Box<dyn Stream<Item = Value> + Send>> per ADR-046 §8, not Vec<Value>.
The Vec shape buffered the entire publish in memory and made the ADR's
flagship use cases (telemetry ingest, live upload, drag-drop file
streaming) impossible. The wire format is unchanged; this corrects API
drift (no deployments exist).

P-05 [major] — publish now registers with timeout: None (like
subscribe), not DEFAULT_CALL_TIMEOUT (30s). A publish whose stream took
>30s wall-clock got a spurious client-side TIMEOUT while the responder
kept consuming. PendingEntry::Call.timeout is now Option<Instant> so the
sweeper never evicts unbounded calls; all register_call callers updated
(Some(...) for call(), None for publish()).

P-08 [major] — the dispatch Pub branch re-implemented invoke_sink's
not-found / visibility / ACL / handler-kind checks inline; invoke_sink
was only ever called from its own tests. Any future fix in invoke_sink
(e.g. the missing publish_schema validation, P-03) wouldn't reach the
wire path. Fix: extract OperationRegistry::resolve_sink_handler
(pub(crate)) as the single source of truth for the sink dispatch checks;
both invoke_sink and Dispatcher::dispatch (Pub branch) call it. The two
paths can no longer diverge.

P-09 [major, side-effect] — make_sink_forwarding_handler in from_call.rs
was store-and-forward (collect into Vec) and silently discarded Err
items (filter_map(|item| item.ok())), contradicting the SinkHandler
contract that an Err terminates the stream. Now that
publish_with_payload takes a Stream, the forwarding handler passes the
stream through directly (streamed, not buffered) and uses
take_while(Ok) + filter_map to terminate on Err. P-09 was listed in
Unit 6 but depends on P-01/P-02, so it falls out naturally here.

P-12 #1 [acceptance gate] — adds the end-to-end publish test that the
review identifies as the gate for this unit:
publish_end_to_end_delivers_chunks_and_returns_response wires
CallConnection::publish ↔ Dispatcher::run_loop over a real
tokio::io::duplex pair (new duplex_connection_pair test helper +
SingleStreamSource BidiStreamSource impl), publishes 3 chunks, and
asserts the responder saw all 3 and returned the right result. A second
test covers the unknown-op → NOT_FOUND path. These tests would have
caught P-01 immediately.

Verification:
- cargo test --lib         → 434 passed, 0 failed (was 432; +2 e2e tests)
- cargo clippy --all-targets -- -D warnings → clean
- cargo fmt --check        → clean
- cargo doc --no-deps      → 4 warnings (pre-existing C-09, unchanged)
2026-08-12 13:29:58 +00:00
glm-5.2 ea66398c88 feat: add Pub operation type, HandlerKind::Sink, call.published wire event (ADR-046)
The call protocol had Subscription (server→client streaming) but lacked
the directional complement: client→server streaming, where the initiator
produces a stream and the responder's handler consumes it. This gap was
inherited from the @alkdev/pubsub EventEnvelope prior art, which has
subscribe but no wire-level publish.

ADR-046 adds the Pub primitive:
- OperationType::Pub (client→server streaming)
- OperationType::Subscription renamed to Sub (wire: "subscription" → "sub")
- SinkHandler type + HandlerKind::Sink variant
- PublishStream type alias (Stream<Item = Result<Value, CallError>>)
- OperationRegistry::invoke_sink() dispatch path
- call.published wire event (sixth event type, additive)
- OperationSpec.publish_schema (Option<Value>, validates per-chunk input)
- DispatchResult::Sink + SinkDispatch (handler future + chunk channel)
- Dispatcher::pump_sink (feeds call.published chunks from wire to handler)
- CallConnection::publish() / publish_with_payload() client methods
- from_call sink forwarding handler (make_sink_forwarding_handler)
- make_sink_handler() helper

Fan-out/broker (one producer, N consumers, topic matching) is deferred to
the channels session — the call protocol is point-to-point; the broker is
a routing concern that sits above it. The Pub primitive is the
load-bearing piece the broker will compose on.

- 23 new tests (366 total, up from 343)
- clippy clean, fmt clean

Verification:
  cargo test                                    — 366 passed
  cargo clippy --all-targets -- -D warnings      — clean
  cargo fmt --check                              — clean
2026-08-12 08:06:46 +00:00
glm-5.2 1ceb9b785d docs: rename pass — update crate refs from alknet-* to alkcall
Update doc comments referencing the old alknet-* crate names and
spec paths to reflect the alkcall home. ALPN strings (alknet/call,
alknet/test) are wire-stable and unchanged. Sibling crate refs
(alknet-http, alknet-core historical context) kept as-is — those are
accurate references to crates that still exist or will be reworked
later.

Also fixes the pre-existing broken intra-doc link [`CallAdapter`] in
dispatch.rs (now [`super::adapter::CallAdapter`]) — the only doc
warning carried over from the extraction.

Verification:
- cargo test: 343 passed (0 failed)
- cargo clippy --all-targets -- -D warnings: clean
- cargo fmt --check: clean
- cargo doc --no-deps: clean (0 warnings, was 1)
2026-08-12 05:58:59 +00:00
glm-5.2 4bc7a19695 feat: extract alknet-call into alkcall, vendor core types
Phase 1 of the alknet-call + alknet-channels unification. The call crate
is extracted verbatim from /workspace/@alkdev/alknet/crates/alknet-call
and the needed alknet-core types are vendored into src/core/ — alkcall is
the home for these types going forward (no separate alkcore crate).

Vendored core types (src/core/):
- auth.rs: Identity, AuthToken, AuthContext, IdentityProvider
- ownership.rs: OwnershipProvider, OwnershipStore, InMemoryOwnershipStore,
  OwnershipError
- types.rs: ProtocolHandler, Connection, BiStream, BidiStreamSource,
  SendStream, RecvStream, HandlerError, StreamError, Capabilities, Secret,
  IdentityAlreadySet
- Stripped: quinn/iroh/rustls deps, Connection::from_quinn/from_iroh
  (the dial lives in the consumer per ADR-089), config/credentials/
  fingerprint/store modules, ConfigIdentityProvider/IdentityStore

Call crate (src/client, src/protocol, src/registry/):
- Copied verbatim from alknet-call; alknet_core::{auth,types,ownership}
  rebound to crate::core::{auth,types,ownership}
- ALPN strings unchanged (alknet/call — wire-stable per ADR-006)
- No behavioral changes

Verification:
- cargo test: 343 passed (0 failed)
- cargo clippy --all-targets -- -D warnings: clean
- cargo fmt --check: clean
- cargo doc --no-deps: 1 pre-existing broken intra-doc link
  (CallAdapter in dispatch.rs — Phase 2 cleanup)

Next: Phase 2 — channels greenfield implementation + ALPN rename +
architecture doc porting.
2026-08-12 05:52:18 +00:00