Agent Action Capsule · explain
16 / 16 · From the September deck

One model split across peers

When one model is split across several Mesh-LLM nodes, each node runs a slice of its layers. The same record pattern then applies one level down: each stage seals what crossed its hand-offs, and the coordinator's record of the request accounts for every stage.

A split request: each stage seals its hand-offs, the coordinator accounts for every stage, the requester compares the digests Four boxes in a row: a requester on the left, then the coordinator C (stage 0, layers 0 to 15), stage 1 on node B (layers 16 to 31) and stage 2 on node D (layers 32 to 47). Step 1: the requester sends one request to C, and C assigns the slices. Step 2: activation frames flow forward from C to B and from B to D; under each hop, both ends keep a running digest of the bytes that crossed it. Step 3: replies flow back hop by hop and the answer returns to the requester. Step 4: B and D each seal a stage record carrying their slice, the package id and per-hop frame counts and digests, with no activations or text, and push it to C over record-push/1; neither learns the requester. Step 5: C seals its main record with its own slice as stage 0, a receipt in which stage 1 and stage 2 are present, and two split_stage references to its own stage-exchange records, and carries the stage records to the requester. Step 6: the requester compares each hop's two digests: hop 0 forward 557e4f equals 557e4f, hop 1 forward 292c1d equals 292c1d, the reply lanes agree, so the hand-offs agree and the request completed. Step 7: in a second vector one byte differs on hop 1's forward lane; the two digests differ and that hop reads break, which shows a disagreement between B and D but not which one is wrong or whether any slice was computed correctly. Requester sends one request, gets one answer Coordinator C stage 0 · its own slice layers 0–15 Stage 1 · node B runs one slice layers 16–31 Stage 2 · node D final slice layers 32–47 assigns slices: C 0–15 · B 16–31 · D 32–47 hop 0 hop 1 forward lane: each end folds the frame bytes as written / as read, before any parse answer reply lane: relayed hop by hop, folded the same way stage record · B stage · index 1 up.recv 557e4f… (4) down.sent 292c1d… (4) + reply lanes key e74b97… stage record · D stage · index 2 up.recv 292c1d… (4) no downstream + reply lane key 57a88d… record-push/1 to the coordinator main record · C own slice: stage 0 receipt: stage 1 present stage 2 present refs: split_stage ×2 deadline 2000 ms main record + stage records Requester's offline check, per hop, per lane hop 0 forward C wrote 557e4f… = B read 557e4f… agree ✓ reply dee44c… = dee44c… agree ✓ hop 1 forward B wrote 292c1d… = D read 292c1d… agree ✓ reply b6eb86… = b6eb86… agree ✓ hand-offs agree ✓ · terminal_state completed · C's own slice gets no ✓ (it would compare C with itself) Vector one_byte_changed: one byte differs on hop 1's forward lane hop 0 forward agree ✓ reply agree ✓ hop 1 forward B's digest ≠ D's digest break ✗ reply agree ✓ hand-offs do not agree · stage cells B and D still ok · which end is wrong is not shown

1What it is

Mesh-LLM can split one model across several nodes: a coordinator runs the opening slice of layers and hands activations on to other nodes, each running the next slice.

The stage-record design turns each coordinator-to-stage pair into an exchange of its own: each stage seals a record of its slice and of the exact bytes that crossed its hand-offs, as digests, and the coordinator's record of the request cites its own record of each stage exchange and names each stage's record by digest in its receipt.

A requester holding those records can check, offline and without contacting any stage, whether both ends of every hand-off committed to the same bytes, and see a missing record as a gap rather than as agreement.

2Design choices

Chosen: nested exchanges: each coordinator-to-stage pair is an ordinary two-sided exchange; the coordinator's main record cites its own record of each stage exchange (split_stage), and its receipt names each stage's record by digest.
Rejected: a new composite record kind holding the whole split; sequence numbers, chaining, record push and pairwise citation would all have to be rebuilt. (capsules design, §5 and Q-D1)
Chosen: hash the frame bytes as the sender wrote them and as the receiver read them off the stream, before any parse.
Rejected: hashing the parsed message or the tensors; the host's codec decodes each activation on read and re-encodes it, so the parsed message is not the bytes that crossed the hop. (capsules design, §2.5)
Chosen: each lane's running digest starts from the coordinator, run, request, hop index and lane (forward, reply, direct return).
Rejected: a plain hash over the frames; a record from one hop could then be replayed as another hop's, and forward and reply frames could be mixed. (capsules design, §4.2; vector replayed_at_other_hop_index)
Chosen: a stage record that did not arrive, or a required lane empty on both ends, reads gap; the request's terminal_state is always shown beside the hand-offs.
Rejected: reading absence or two empty lanes as agreement, or agreement as completion; a request that stopped early can have both neighbours agree on a truncated stream. (capsules design, §7.2; vector stopped_early_agrees_on_truncated_stream)
Chosen: stages are never told the end requester, and the record leaves out the session id.
Rejected: carrying the session id, which stays stable across a session and would link one party's requests over time in any record shown to others. (capsules design, §4.3 and Q-D4)
Chosen: the host emits one event per stage per request, best-effort, and serving never waits on it; with no plugin declaring the channel, nothing is hashed.
Rejected: the plugin tapping the stage stream itself; split serving has no per-request hook, and the digest must sit below the codec's parse, which only the host can reach. (capsules design, §8)

3What it shows / what it does not

Shows

  • Which signing key claims which slice of the request: layer range and package id, per request, signed.
  • For each hop and lane, whether both ends committed to the same bytes: agree, break, gap, malformed or not_applicable.
  • One end misreporting, or a hand-off altered in transit, as a break on that hop, without disclosing any activation.
  • A withheld or late stage record as absent and a gap, never as agreement.
  • Whether the coordinator's assignment and the stage's own record name the same slice (disagrees when they do not).
  • Two different records sealed for one stage of one request, as conflict.

Does not show

  • That any stage computed its slice correctly; equal digests show both ends saw the same bytes, not that the output is the right function of the input.
  • Collusion: two neighbouring stages can report matching digests for anything, and a stage that alters its own output and reports it consistently is not caught.
  • That the stages are independent: a coordinator can place every stage on keys it controls.
  • That a stage loaded the weights it names; no per-slice weights digest exists, so package id and layer range are a signed claim.
  • Which end is wrong in a break.
  • Who operates a stage's key; the requester's check does not yet link a stage key to the assigned node.

4Example

Stage 1's record from the plugin's split-stage fixture, three stages, replies relayed (other envelope fields, the signature, the nonce and the assurance and disposition blocks trimmed). Its upstream.received digest equals the coordinator's downstream.sent, and its downstream.sent equals stage 2's upstream.received: the two forward hand-offs agree.

{
  "capsule_id": "ee550164e1c49cf5ba0f91ce1b5acc98d7d41a10c343639d88085ca83243b9cd",
  "spec_version": "draft-mih-scitt-agent-action-capsule-05",
  "canonicalization_id": "jcs",
  "action_id": "mesh-poc/split-stage/stage/node-coordinator-c/mesh-split-1790000000000000000-g2/18446744073709551615/1",
  "model_attestation": {
    "model_id": "n/a-split-stage",
    "provider": "mesh-llm",
    "compute_attestation": {
      "x-mesh-poc-v1": { "role": "stage" },
      "x-mesh-stage-v1": {
        "v": 1,
        "side": "stage",
        "coordinator_node_id": "node-coordinator-c",
        "run_id": "mesh-split-1790000000000000000-g2",
        "request_id": "18446744073709551615",
        "topology_hash": "topology-hash-0001",
        "stage_index": 1,
        "stage_count": 3,
        "layer_start": 16,
        "layer_end": 32,
        "package_id": "sha256:abab…abab",
        "manifest_sha256": "cdcd…cdcd",
        "source_model_sha256": "efef…efef",
        "return_mode": "relayed",
        "upstream": {
          "received":     { "frames": 4, "digest": "sha256:557e4faf6e4fb73578f5715bb0a7a71b659adb949699d5e0d71702162c3db6d3" },
          "replies_sent": { "frames": 3, "digest": "sha256:dee44caeb95e4142b5bc09274cd08252bc5025dfde02842eda5af77e93a85c44" }
        },
        "downstream": {
          "sent":             { "frames": 4, "digest": "sha256:292c1d551a824014fbca8353a8ed0dd417286036d3fe7bc27ac97dac81774e51" },
          "replies_received": { "frames": 3, "digest": "sha256:b6eb868e31e55d57db915c87158b2b86aece54321a0b3cb306a774581d557161" }
        },
        "direct_return": null,
        "tokens": { "prefill_received": 12, "decode": 4 },
        "terminal_state": "completed"
      }
    }
  },
  …
  "key_id": "e74b97f06c437c8056392be8ab7b7da541a89d69d1871d8b4665513ec6f0d231"
}
FieldTypeMeaning
x-mesh-poc-v1.rolestringstage: a record of a coordinator-to-stage exchange; a reader that does not know it reads unknown, never served.
sidestringstage for the stage's own record, coordinator for the coordinator's side and its own slice.
coordinator_node_id, run_id, request_idstringThe split key; the coordinator id is included because the coordinator mints the run id and nobody checks it. request_id is a decimal string so a u64 survives every JSON reader.
stage_index, stage_countintegerThis slice's place in the chain; stage 0 is always the coordinator.
layer_start, layer_endintegerThe slice; layer_end is exclusive, so this is layers 16–31.
package_id, manifest_sha256, source_model_sha256stringThe model package, its manifest and the source model the stage says it loaded; all package-wide, none per slice (fixture values here).
return_modestringrelayed (replies hop by hop) or direct (the final stage returns to stage 0); decides which lanes must be non-empty.
upstream / downstreamobjectPer hop, per lane: frame count and running digest of the bytes this stage read or wrote. Stage 0 has no upstream; the final stage has no downstream.
tokens.prefill_receivedintegerTokens received for prefill, including any restored from cache; counts, never content.
terminal_statestringHow the request ended at this stage; shown beside the hand-offs, since agreement is not completion.
key_idhexThe stage's own signing key; stage 2's record is signed by a different key (57a88de9…).

Source: Mesh-LLM/capsules:vectors/split-stage/rust-split-bundle.json (key split_stage_records[0]) @ efcada74026a2e18b9e62d77432888a31bbe9de4; the break case: vectors/split-stage/hop-cases.json, case one_byte_changed, same SHA. The fixture is sealed by the plugin's own generator test, not taken from a live run.

5Status

PieceStatusEvidence
Stage record shape and the hop digestbuilt, not livecrates/evidence-plugin/src/producer/stage.rs; byte layout pinned in vectors/split-stage/fold-vectors.json. Mesh-LLM/capsules @ efcada74026a2e18b9e62d77432888a31bbe9de4, also in v0.1.4 (b4e24453f572b2d9fe8f2cefbd0f35a5eb54b0ad).
Stage seals and pushes to the coordinator; coordinator waits (2000 ms default), seals the receipt and split_stage references, carries the stage records to the requesterbuilt, not livecrates/evidence-plugin/src/split_stage.rs @ efcada74026a2e18b9e62d77432888a31bbe9de4, fed by fixture events; its header reads "No host emits skippy.stage.v1 yet", and the channel is not declared in the plugin's manifest.
The requester's hop check (agree, gap, break, malformed, not_applicable)built, not livecrates/evidence-plugin/src/producer/stage_verify.rs and the 16 cases in vectors/split-stage/hop-cases.json @ efcada74026a2e18b9e62d77432888a31bbe9de4.
Console stage strip and the "served you through a split" Peers groupbuilt, not liveweb-ui/src/features/capsules/components/StageStrip.tsx and LedgerPeersTable.tsx @ efcada74026a2e18b9e62d77432888a31bbe9de4; unit-tested against the split-stage vectors only: no pane serves split rows yet (web-ui/src/features/capsules/api/sidecarTypes.ts, design §12.2).
Nesting parent and child rows, role filter chipsdesigned, not builtdocs/DESIGN-split-stage-records.md §12.2 @ efcada74026a2e18b9e62d77432888a31bbe9de4.
Host event skippy.stage.v1, one per stage per requestplannedListed as "proposed" in the capsules README's host-changes table @ efcada74026a2e18b9e62d77432888a31bbe9de4. Upstream Mesh-LLM/mesh-llm main @ 60697c61755b7280c22d3ab9d9bcf5b663e8ae3b: the callers of broadcast_channel_message in mesh/crates/mesh-llm-host-runtime/src/ send openai.exchange.v1 (plugin/openai_exchange.rs), payment.lifecycle.v1 (network/payments/lifecycle.rs) and routing.choice.v1 (network/peer_blocks.rs); none sends a stage channel. (Upstream already uses the name skippy.stage.v1 for its stage wire protocol's protobuf package; that is not a plugin channel.)
Per-request salt for the digestsdesigned, not builtDesign §4.4 and §8 part B; v0 hashes a zero-length salt (§12), so stage records stay with the coordinator and the requester.
A referee re-running one stage's slice as a spot checkplannedDesign §9: would compare decoded tensors within a stated tolerance, not digests.

6Specs

No Internet-Draft defines stage records yet; the record shape and the hop digest are the plugin's design document. The record itself is an ordinary capsule under these specs.