Skip to content

Routed correspondence from Wuxi

A commander in Wuxi carries a sealed letter for one recipient. The program runs the delivery twice: first the commander knows the recipient’s address as a Wuxi delivery district, then as a Beijing one. Each route is planned from the commander’s own knowledge of stations, timetables, and addresses.

The question the example answers: how does a message cross a transport network using only what its carrier knows, with the route and each delivery step recorded?

The network is synthetic. Station names, timetables, and travel times are demonstration data.

  • The canwu-correspondence domain extension. Its CorrespondencePlugin runs next to InformationPlugin from canwu-information. A correspondence (CorrespondenceOperation) is one addressed message plus its delivery and transport.
  • Holder-relative knowledge: stations, connections, and addresses live in one holder’s ledger (KnowledgeHolderRef), and planning reads that ledger at a recorded read cut (KnowledgeReadCut). See reading holder knowledge.
  • plan_route from canwu-routing and TransportExecution from canwu-transport, which runs the legs and records a Handoff between them. See routing and transport.
  • The information lifecycle from canwu-information: a Dispatch and its DeliveryAttempt. See encoded interception.
  • An automatic communication opportunity decided by operation-keyed random draws.
Terminal window
cargo run -p canwu-correspondence --example routed_correspondence

Output:

Wuxi local delivery: 1 leg(s), arrival minute 30
wuxi/hub -> wuxi/delivery/recipient via Horse
Wuxi to Beijing: 2 leg(s), arrival minute 3060
wuxi/hub -> beijing/station via Rail
beijing/station -> beijing/delivery/recipient via Horse

The arrival minute is the plan’s estimated_arrival_at, counted from the start of the run. The Beijing plan takes the direct train at hour 1, reaches Beijing station two days later at minute 2940, and adds two hours on horseback: 2940 + 120 = 3060. The route through Nanjing would reach Beijing station a day later.

Flow of one correspondence from carrier knowledge to delivery, with incidents and recovery. View diagram source.
View diagram source
flowchart TD
  K["Carrier's ledger (the sender here): stations, timetables, address"]
  O["Communication opportunity or decision ticket"]
  C["initiate_correspondence_v1"]
  P["RoutePlan from the ledger at a read cut"]
  A["Dispatch active, DeliveryAttempt created"]
  L["Transport legs, Handoff at each transfer stop"]
  D["Attempt Delivered, dispatch Completed"]
  X["Disaster: new ItineraryRevision for the same attempt"]
  I["Interception: Access record, delivery continues"]
  F["Carrier seized or late arrival: attempt Failed"]
  R["resolve_correspondence_v1 from the sender"]
  O --> C --> P
  K --> P
  P --> A --> L --> D
  L -. disaster .-> X
  X -. reroute .-> L
  L -. interception .-> I
  L -. seizure or deadline .-> F
  F -.-> R
  R -. retry with a new attempt .-> P

Solid arrows are the path the example runs. Dotted arrows are incidents and recovery, covered by the tests described at the end of this page.

The code is in routed_correspondence.rs and its fixture examples/support/mod.rs. run(long_distance) builds a fresh simulation per request. The 1940 passed to Canwu::new_with_plugins is the random seed.

scenario_with_prepared_dispatch takes the commander (sender) and the observer (recipient) from the deprecated Canwu::demo(1) compatibility scenario, then adds a sealed-letter channel, the letter content, its representation, and a dispatch:

&DispatchPayload {
status: DispatchStatus::Prepared,
target: DispatchTarget::Addressed(vec![recipient.clone()]),
prepared_at: snapshot.initial_time,
dispatched_at: None,
completed_at: None,
},

The plugin accepts only a Prepared dispatch with this single recipient and the same sender and channel profile as the request.

network_seed returns a NetworkKnowledgeSeed. The Beijing version knows a direct train, a route through Nanjing, and two final-mile roads. The direct train uses TraversalModel::Departures, with departures at hour 1 on days 0, 5, and 10:

connection_departure(
"wuxi-beijing-direct",
"wuxi/hub",
"beijing/station",
TransferMode::Rail,
departure,
SimDuration::days(2),
),

The seed also holds a KnownAddress that maps the recipient to beijing/delivery/recipient (or wuxi/delivery/recipient). run submits the seed as KNOWLEDGE_INGRESS for KnowledgeHolderRef::Person(sender), and the plugin publishes each endpoint, connection, and address as a knowledge record in the sender’s ledger.

In the same boundary, run submits OPPORTUNITY_INGRESS:

CommunicationOpportunityRequest {
operation_key: operation_key.to_owned(),
sender: EntityRef::Person(sender),
candidates: vec![recipient.clone()],
reason: "routine correspondence".to_owned(),
probability_per_mille: 1_000,
automatic: true,
}

The plugin makes one operation-keyed draw to decide whether the opportunity occurs and another to pick a recipient from candidates, so replay gets the same result. With 1_000 per mille and one candidate, the observer is always selected, and the opportunity record gets status SelectedAutomatic.

let request = InitiateCorrespondenceRequest {
operation_key: operation_key.to_owned(),
sender: EntityRef::Person(sender),
recipient,
carrier: KnowledgeHolderRef::Person(sender),
channel_profile: "sealed-letter".to_owned(),
origin: RoutingNodeRef::new("wuxi/hub"),
due_at: canwu.time() + SimDuration::days(10),
prepared_dispatch,
delivery_attempt_operation: InformationOperationId::new(
"example.correspondence",
format!("{operation_key}-attempt"),
),
routing_policy: canwu_api::RoutingPolicy::default(),
capacity_admission: CorrespondenceCapacityAdmission::Unconstrained,
execution_id: canwu_api::TransportExecutionId(if long_distance { 2 } else { 1 }),
automatic_opportunity: Some(opportunity_ref(operation_key)),
carrier_delegation: None,
};

correspondence_command wraps it as the initiate_correspondence_v1 plugin command, issued by Issuer::System with CommandAuthority::no_responsible_actor(...). The plugin accepts that authority only with a SelectedAutomatic opportunity for the same key, sender, and recipient. The carrier is the sender, so carrier_delegation is None.

settle_start in plugin.rs reads the carrier’s ledger, resolves the address, and plans:

let (snapshot, address) = carrier_planning_snapshot(
view,
&admitted.request.carrier,
&admitted.request.recipient,
context.at,
)?;
let route_plan = plan_route(
&snapshot,
&RoutingRequest {
origin: admitted.request.origin.clone(),
destination: address.destination.clone(),
departure_at: context.at,
policy: admitted.request.routing_policy.clone(),
},
)
.map_err(|error| invalid_record(error.to_string()))?;

The default RoutingPolicy picks the earliest arrival. The plugin stores a CorrespondenceOperation with the plan, the address, the read cut, and planning evidence, marks the opportunity Consumed, and asks the information plugin to activate the dispatch. Activation creates delivery attempt 1 with the request’s due_at.

The plugin schedules its own ProgressAction::StartLeg and CompleteLeg ingress at each leg’s planned times and rejects progress submitted by the host. When a leg before the last completes, complete_leg records a planned Handoff at that stop; the Beijing run records one at beijing/station. When the last leg arrives:

let status = if context.at <= operation.current_due_at {
DeliveryAttemptStatus::Delivered
} else {
DeliveryAttemptStatus::Failed
};

A delivered attempt completes the dispatch, and the operation becomes CorrespondenceStatus::Settled. run steps up to 80 boundaries until typed_domain_record(&correspondence_operation_ref(operation_key)) shows Settled, and print_plan prints the route_plan.

  • Both requests run through the same plugin code. Only the seed data and the address differ.
  • The plan uses only connections in the carrier’s ledger.
  • RoutePlan.estimated_arrival_at is an estimate; DeliveryAttempt.due_at is the deadline. A late arrival fails the attempt, marks the operation DeadlineMissed, and keeps the original due_at.
  • Each step leaves a record: the planning history with read cuts, the transport legs and handoffs, and versioned dispatch and attempt records.
  • Remove the direct train. Delete wuxi-beijing-direct in network_seed. The Beijing plan becomes three legs through nanjing/station and arrives at minute 4500: Nanjing at hour 25, Beijing station at hour 73, then two hours on horseback.
  • Miss the deadline. Set due_at to canwu.time() + SimDuration::days(1), return from the loop once operation.status.is_terminal(), and print operation.status. The local letter settles. The Beijing letter arrives at minute 3060, after the deadline at minute 1440, and ends as DeadlineMissed.
  • Suppress the opportunity. Set probability_per_mille to 0. The opportunity is stored as Suppressed, the plugin rejects the command, and run panics because no operation appears.

The example stops once both letters settle. The rest of the lifecycle is in the crate and is exercised by its tests: routed_delivery.rs covers decision-backed sending, incidents, and recovery; gap_g25_correspondence_delegated_carrier.rs covers delegated carriers; and gap_g26_correspondence_carrier_seized.rs covers carrier seizure. Most of these tests end by restoring a snapshot and rebuilding the run with Canwu::replay_from_journal (exact replay).

  • Sending by decision. correspondence_decision_ticket builds a decision ticket whose send option carries the command. The plugin accepts it when the decision origin is the sender; routed_delivery.rs uses this path.
  • Incidents. The application submits a CorrespondenceIncidentRequest with a probability_per_mille through INCIDENT_INGRESS. The plugin rolls it with an operation-keyed draw and records it on the operation; an incident that arrives in an inapplicable state is kept as suppressed evidence.
    • Disaster fails the current leg and replans from the current stop without the blocked connections. The result is a reroute: a successor ItineraryRevision for the same attempt. With no known route, the operation waits as WaitingForRoute.
    • Interception writes an Access record for the interceptor. Delivery continues.
    • CarrierSeized { seized_by, custody_handoff } is a carrier seizure. The leg fails, a terminal HandoffKind::Seizure handoff is recorded, and the attempt and its transport execution close as failed. Only the carrier receives an attempt_report knowledge record, and it omits the seizer.
  • Recovery. The sender’s decision-backed resolve_correspondence_v1 command takes one CorrespondenceRecoveryAction. ReplanCurrentAttempt continues a WaitingForRoute attempt after the carrier learns new connections. RetryDelivery starts a delivery retry: a new attempt that references the old one under the DomainReference role "previous_attempt", with a new deadline and TransportExecution. FinalizeDispatch completes the dispatch, and the operation ends as Failed or DeadlineMissed. Until then, a failed attempt leaves the dispatch Active.
  • Delegated carriers. A delegated carrier first issues delegate_carrier_v1 under its own command authority. Its DelegationClaimV1 names the carrier as performed_by, the sender as performed_for, and the carry_correspondence capability. From the next boundary the sender cites that command in InitiateCorrespondenceRequest::carrier_delegation. Planning then reads the carrier’s ledger, and the engine publishes none of it to the sender. The claim must cover each dispatch, including retries, and a newer delegation from the same carrier to the same sender replaces the older one.
  • Capacity. CorrespondenceCapacityAdmission has one variant, Unconstrained: planning and delivery ignore transport capacity. Capacity pools belong to canwu-movement; see routing and transport.

Open the runnable example

Read the delivery, reroute, and retry tests

Read the delegated carrier test

Read the carrier seizure test