Files
alkcall/docs/architecture/decisions/014-irpc-never-integrated-hand-rolled-framing.md
glm-5.2 cc470a363a docs: port architecture specs + 45 ADRs from alknet, renumbered
Port the call + channels architecture documentation from the alknet
mono-repo into docs/architecture/, renumbered as alkcall ADR-001..045.

Renumbering map (alknet -> alkcall):
  Core:        001,002,004,006,007,011,065,070,092,014,050,091 -> 001-012
  Call:        005,064,012,023,015,022,024,016,049,017,028,029,030,032,066,069,067,068 -> 013-030
  Shared:      003,009,013 -> 031-033
  Channels:    071,093,072,073,074,075,076,094,079,080,081,089 -> 034-045

3 superseded/reversed ADRs kept for historical trail:
  - ADR-013 (irpc foundation, superseded by ADR-014)
  - ADR-023 (peer-scoped filtering, superseded by ADR-024)
  - ADR-077 (TTY inside channels, reversed by ADR-035 — not ported, TTY-only)

Ported docs (11 spec files + README + open-questions):
  - call-README.md, call-protocol.md, operation-registry.md, client-and-adapters.md
  - channels-README.md, channels-overview.md, channels-wire.md, channels-connection.md, channels-adapter.md, channel-operations.md, channel-client.md
  - README.md (index with doc table, ADR table grouped by category, key principles)
  - open-questions.md (lean — 30 OQs, renumbered OQ-01..030; includes new OQ-22 for the pub/sub gap)

Cross-reference rewriting:
  - All ADR-NNN references rewritten single-pass (no chaining bug)
  - Markdown link paths fixed
  - Title lines aligned with filenames
  - Non-ported ADR refs (052, 082, 086, etc.) left as-is with README note

The open-questions.md includes OQ-22 (new): the call protocol pub/sub
gap — subscribe exists but pub does not, needed for channels
channel/resources/subscribe fan-out. This is the next ADR to write
(alkcall ADR-046).
2026-08-12 07:06:57 +00:00

8.4 KiB

ADR-014: irpc Was Never Integrated — Hand-Rolled EventEnvelope Framing

Status

Accepted

Context

ADR-013 accepted "irpc as the call protocol foundation" based on the previous architecture's use of irpc. When the call protocol was implemented, it turned out that no .rs file in the workspace ever imported irpc. The workspace Cargo.toml declared irpc = "0.16" / irpc-derive = "0.16" as workspace dependencies, and crates/alknet-call/Cargo.toml declared irpc = { workspace = true }, but the import was never written. The wire protocol (crates/alknet-call/src/protocol/wire.rs) is hand-rolled length-prefixed JSON — 4-byte big-endian length prefix + UTF-8 JSON body — not an irpc service.

This was discovered when building an external app against the crates: the irpc 0.16 workspace dep was a version-gap blocker for alknet-blobs (which pulls irpc 0.17 transitively via iroh-blobs 0.103). A grep for any irpc import in the workspace found zero hits — the dep was dead weight carried over from the previous architecture without verification.

The framing, operation registry, dispatch, and subscription patterns that ADR-013 attributed to irpc are all hand-rolled in alknet-call:

  • Framing: FrameFramedReader / FrameFramedWriter in wire.rs — length-prefixed JSON, hand-written against tokio::io::AsyncRead/ AsyncWrite. Not an irpc service.
  • Operation registry: OperationSpec, Handler, OperationRegistry, AccessControl — hand-rolled in alknet-call, not irpc's Service trait.
  • Event types: call.requested, call.responded, call.completed, call.aborted, call.error — the alknet call protocol's own event vocabulary, not irpc's.
  • Subscription/streaming: StreamingHandler / invoke_streaming() (ADR-021) — hand-rolled, not irpc's streaming patterns.

The EventEnvelope { type, id, payload } shape was derived from the @alkdev/pubsub TypeScript EventEnvelope (/workspace/@alkdev/pubsub/src/ types.ts), not from irpc. ADR-013's claim that "the wire format is irpc's EventEnvelope framing" was wrong — irpc was never imported, and the envelope shape has a different origin (the pubsub prior art, ADR-033). The framing coincidentally resembles irpc's (both are length-prefixed JSON), which is how the misattribution went unnoticed.

What ADR-013 got right

Despite the irpc misattribution, ADR-013's architectural decisions are correct and stand unchanged:

  • The call protocol uses length-prefixed JSON EventEnvelope framing (hand-rolled, not irpc-supplied).
  • The wire format is cross-language and consumable from TypeScript, Python, any language (JSON is inherently cross-language — ADR-013's "mitigated: it's length-prefixed JSON" note was the load-bearing point, not the irpc attribution).
  • Operations use JSON Schema discovery. The OperationSpec shape is hand-rolled, JSON-Schema-compatible — the same property ADR-013 attributed to irpc, achieved without irpc.

Why a new ADR rather than an amendment

ADR-013's Decision and Consequences are built on the premise "alknet-call uses irpc as its foundation — irpc IS the call protocol's core." That premise is false. Amending ADR-013 to say "actually it's hand-rolled" would leave an ADR whose Context, Decision, and Consequences sections all argue for a choice that was never made. The correct record is: ADR-013 is superseded; the call protocol uses hand-rolled framing (this ADR-014); the architectural properties ADR-013 sought (proven framing, cross-language JSON, streaming) are preserved, but the mechanism is hand-rolled, not irpc-sourced.

The dead dep removal

The irpc / irpc-derive workspace deps and the alknet-call consumer dep were removed in commit 668d777 (2026-07-09). irpc may be re-added as 0.17 when alknet-blobs lands (it pulls irpc 0.17 transitively via iroh-blobs 0.103), but that would be a transitive dependency of alknet-blobs, not a direct dependency of alknet-call — alknet-call does not import irpc and has no plans to. See docs/research/transport-generalization/findings.md §3.1 for the removal trace.

Decision

  1. ADR-013 is superseded. The call protocol does not use irpc. irpc was never imported by any .rs file in the workspace. The dead irpc / irpc-derive workspace and crate deps are removed.

  2. The call protocol uses hand-rolled EventEnvelope framing. The wire format is length-prefixed JSON (4-byte big-endian length + UTF-8 JSON body), implemented in crates/alknet-call/src/protocol/wire.rs. The EventEnvelope { type, id, payload } shape was derived from the @alkdev/pubsub TypeScript prior art (ADR-033), not from irpc. The framing, operation registry, dispatch, and streaming patterns are all hand-rolled in alknet-call.

  3. The architectural properties ADR-013 sought are preserved by the hand-rolled implementation:

    • Proven framing — length-prefixed JSON is a well-understood, battle-tested pattern; the hand-rolled implementation is tested (207 lib + 2 integration tests passing).
    • Cross-language — JSON is inherently consumable from any language; NAPI, WASM, and browser clients speak the same wire format.
    • Streaming — StreamingHandler / invoke_streaming() (ADR-021) provide the subscription/streaming patterns ADR-013 attributed to irpc, hand-rolled.
  4. irpc is not a planned dependency for alknet-call. If alknet-blobs pulls irpc transitively, it will be a transitive dependency of that crate, not a direct dependency of alknet-call. alknet-call's framing, registry, and dispatch are hand-rolled and will remain so. The "mitigated: irpc is lightweight and we can fork if needed" caveat in ADR-013 is moot — there is nothing to fork because nothing was integrated.

  5. The vault's irpc drop (ADR-025) stands. ADR-025 dropped irpc from alknet-vault. With this ADR, irpc is also confirmed absent from alknet-call. The vault and call decisions are now consistent: neither crate uses irpc. The only difference is that ADR-025 removed a real (but unused-for-its-primary-path) irpc dependency from the vault, while this ADR records that alknet-call's irpc dependency was never integrated at all — it was a Cargo.toml entry with no corresponding import.

Consequences

Positive:

  • The spec matches the code. ADR-013's irpc claims were a spec/code divergence that surfaced only when the irpc 0.16 version gap blocked alknet-blobs. This ADR closes the divergence.
  • alknet-blobs is unblocked — the dead irpc 0.16 workspace dep is gone; iroh-blobs 0.103 (which pulls irpc 0.17 transitively) no longer conflicts with a workspace-pinned older irpc.
  • The call protocol's framing, registry, and dispatch are documented accurately as hand-rolled — readers of the spec aren't sent looking for an irpc integration that doesn't exist.
  • The cross-language story is unchanged: JSON wire format, JSON Schema discovery. The mechanism changed (hand-rolled vs irpc), but the property ADR-013 sought is preserved.

Negative:

  • The call protocol does not inherit irpc's testing or production pedigree for its framing. Mitigation: length-prefixed JSON is a trivial, well-understood pattern; the hand-rolled implementation is tested; and the framing is small enough to audit completely (~30 lines in wire.rs).
  • ADR-013's claim that "the call protocol inherits irpc's streaming and subscription patterns" was wrong — those patterns are hand-rolled (ADR-021). The streaming implementation is younger and less battle-tested than irpc's would have been, but it is also simpler and fully owned.

References

  • ADR-013: irpc as call protocol foundation (superseded by this ADR)
  • ADR-025: Vault local-only dispatch (dropped irpc from the vault; this ADR records irpc was never integrated into alknet-call either)
  • ADR-033: Rust as canonical implementation (the @alkdev/pubsub prior art that the EventEnvelope shape was actually derived from)
  • ADR-021: Streaming handler for subscriptions (the hand-rolled streaming dispatch path)
  • Call protocol wire format: crates/alknet-call/src/protocol/wire.rs
  • Transport generalization findings: docs/research/transport-generalization/findings.md §3.1 (dead irpc dep removal)
  • Removal commit: 668d777 (2026-07-09)