Files
unshell/unshell-protocol/src/protocol/PROTOCOL_CHANGES.md
T
Michael Mikovsky d4100d0604 Split protocol and leaf surfaces into crates
Move the protocol runtime into unshell-protocol and remote shell leaf code into unshell-leaves so endpoint and TUI roles can compile independently without circular dependencies.
2026-04-26 12:39:06 -06:00

6.5 KiB

Protocol Change Pressure

This document records protocol-spec changes that are worth considering after the runtime rewrite in src/protocol.

The current rewrite intentionally keeps the existing wire model from /home/astatin3/Documents/GitHub/unshell/PROTOCOL.md wherever possible. The main goal was to remove avoidable runtime work without silently drifting the protocol.

The implementation now does the following:

  • compiles child routing prefixes once instead of scanning child paths on every packet
  • routes from the header first, then decodes payloads only on local delivery
  • keeps pending hook state minimal and active hook state directly indexed
  • separates local typed send paths from framed transport-facing send paths

Those are implementation changes. They do not require a protocol update.

Implemented Deviation

The current scratch rewrite does deviate from the frame format described in PROTOCOL.md Section 8.

The old format used one u32 length prefix immediately before each archived section. The new implementation uses one aligned two-section frame:

  • u32 header_len
  • u32 payload_len
  • aligned archived header bytes
  • aligned archived payload bytes

The payload start is padded up to the canonical archive alignment boundary.

This deviation was made explicitly because the prior layout baked in alignment repair complexity and extra decode copies even in an otherwise clean runtime.

No Immediate Semantic Change Required

Aside from the framing change above, the current runtime rewrite does not require a semantic protocol break.

The following parts of PROTOCOL.md remain worth keeping as-is:

  • path-based routing remains the canonical behavior
  • pending call context remains distinct from active hook state
  • Fault remains upstream-only
  • unknown or expired hook_id still drops returned traffic
  • hook closure still requires both sides to send end_hook = true, or one Fault

Those rules keep the protocol boring and interoperable.

Change 1: Framing That Guarantees Archive Alignment

Current problem

PROTOCOL.md Section 8 fixes a framed format with a 4-byte big-endian length prefix before each archived section.

That is simple, but it has one hard performance downside in the current Rust implementation:

  • the start of the archived section is not guaranteed to satisfy rkyv alignment
  • the decoder therefore has to copy header bytes into an AlignedVec before safe access
  • local payload decode also copies the payload bytes into another AlignedVec

This means the runtime still performs unavoidable memory copies during decode even after the architectural cleanup.

Revise the framing rules so each archived section begins at a guaranteed aligned offset.

Two viable options:

  1. Add explicit padding after each length field so the archived section begins at the required alignment boundary.
  2. Replace the current two-section frame with one canonical aligned envelope type whose internal layout already satisfies the archive alignment rules.

Why this is objectively better

  • removes the forced alignment-copy step on decode
  • makes zero-copy or near-zero-copy archived access actually achievable
  • reduces local delivery latency for all packet types
  • reduces transient allocation pressure in the decoder

Tradeoff

This is a wire-format change. Every compliant implementation would need to adopt the new framing.

Status

Implemented in the current rewrite.

Change 2: Compact Path Representation for a Future v2

Current problem

PROTOCOL.md Sections 5, 6, 10, 11, and 13 make paths canonical on the wire as Vec<String> values.

That is easy to understand and debug, but it imposes real cost:

  • path routing requires segment-wise string comparison
  • hook state keys carry owned path vectors
  • packets repeat full path strings over and over
  • the runtime must repeatedly compare or clone path structures at boundaries

The new implementation minimizes those costs internally, but it cannot eliminate them while the wire format remains path-string based.

For a future protocol version, consider separating:

  • the canonical human-readable control/discovery layer
  • the compact transport/runtime layer

The compact transport/runtime layer would use stable numeric endpoint IDs instead of repeated Vec<String> path payloads.

Why this is objectively better

  • routing becomes integer-based instead of string-prefix based
  • hook keys become compact and cheap to index
  • packets shrink
  • path comparisons and many path clones disappear from the hot path

Tradeoff

This is a full protocol-versioning decision, not a local cleanup.

It adds coordination costs:

  • peers must agree on endpoint IDs
  • topology updates become more structured
  • the protocol becomes less self-describing on the wire

Recommendation

Do not make this change as a silent update to the current protocol.

If pursued, it should be introduced explicitly as a v2 protocol, because it is no longer behaviorally equivalent to the current path-based wire model.

Change 3: Clarify Caller-Side Hook Activation Semantics

Current problem

PROTOCOL.md Section 13 is explicit about callee-side pending call context, but it leaves more room for interpretation on the caller side after a Call is sent.

The current runtime keeps caller-side hook state available immediately after send so it can validate returned traffic efficiently.

That is practical, but the spec could be clearer about whether the caller's local hook record is considered active immediately, or merely reserved until the callee accepts.

Clarify caller-side wording in Section 13 so implementations know whether the caller may allocate directly into active host state after sending a Call, as long as early returned Data for an actually inactive hook is still discarded per Section 14.1.

Why this is objectively better

  • removes ambiguity for optimized runtimes
  • makes caller-side hook bookkeeping more consistent across implementations
  • avoids accidental spec drift through inference

Tradeoff

This is a clarification change, not necessarily a wire-format change.

Summary

The runtime rewrite shows that most of the original performance problems were architectural, not inherent to the protocol.

The current protocol can support a much lower-loop implementation than before.

The main remaining protocol-level blocker is the framing/alignment rule. That is the one change most worth making if the next goal is to reduce unavoidable memory copies further.