Skip to content

C++ Spot Per-Language Interface

C++ per-language interface table of contents

Session–Actor binding §8.2 owns relocation route updates for a bound Session.

1. Spot Identity And Relocation Registration

The User/Instance Spot factory configure callback specifies exactly one of disable_relocation(), recreate_on_relocation(), or preserve_state_with<TAdapter>().

namespace zlink::framework {

template <typename TSpot>
class spot_relocation_adapter_t {
public:
    virtual ~spot_relocation_adapter_t() = default;
    virtual task_t<std::vector<std::byte>> capture(
      TSpot &spot,
      std::stop_token operation_cancellation) = 0;
    virtual task_t<void> restore(
      TSpot &spot,
      std::vector<std::byte> payload,
      std::stop_token operation_cancellation) = 0;
};

template <typename TSpot>
class user_spot_factory_builder_t {
public:
    user_spot_factory_builder_t &set_stable_type_limit(std::int32_t limit);
    user_spot_factory_builder_t &set_execution_mode(user_spot_execution_mode_t mode);
    user_spot_factory_builder_t &set_relocation_coordination_mode(
      spot_relocation_coordination_mode_t mode);
    void disable_relocation();
    void recreate_on_relocation();
    template <typename TAdapter>
      requires std::derived_from<TAdapter, spot_relocation_adapter_t<TSpot>>
    void preserve_state_with();
};

template <typename TSpot>
class instance_spot_factory_builder_t {
public:
    instance_spot_factory_builder_t &set_stable_type_limit(std::int32_t limit);
    void disable_relocation();
    void recreate_on_relocation();
    template <typename TAdapter>
      requires std::derived_from<TAdapter, spot_relocation_adapter_t<TSpot>>
    void preserve_state_with();
};

} // namespace zlink::framework

In preserve_state_with<TAdapter>(), TAdapter must implement spot_relocation_adapter_t<TSpot>. Passing an Actor adapter, or an adapter that doesn't match the Spot factory, fails as a configuration error before socket bind. The adapter exchanges application state only as an opaque byte vector, and doesn't expose typed state, a separate contract identifier, or a message wrapper.

The declaration of the factory registration member is owned by Channel messaging's mesh_node_builder_t.

2. Spot Framework API

The Framework Spot surface is based on the owner MeshNode and zlink::framework::spot_t.

namespace zlink::framework {

enum class spot_kind_t {
    invalid = 0,
    entry = 1,
    user = 2,
    instance = 3
};

enum class spot_close_reason_t {
    explicit_close = 0,
    host_shutdown = 1,
    relocation_out = 2,
    idle_evicted = 3
};

struct spot_closing_context_t final {
    spot_close_reason_t reason;
    std::chrono::system_clock::time_point deadline;
};

using spot_id_t = std::string;

class spot_ref_t final {
public:
    spot_ref_t(spot_id_t spot_id,
      std::uint64_t object_generation,
      std::string mesh_name,
      node_rid_t node_rid);

    const spot_id_t &spot_id() const noexcept;
    std::uint64_t object_generation() const noexcept;
    std::string_view mesh_name() const noexcept;
    const node_rid_t &node_rid() const noexcept;
};

class spot_context_t;
class entry_spot_context_t;
class instance_spot_context_t;
class spot_handler_registry_t;
class instance_spot_handler_registry_t;
struct spot_actor_join_result_t;
struct actor_create_response_t;
struct spot_create_response_t;

enum class spot_relocation_ready_outcome_t : std::uint8_t {
    continued = 0,
    relocated = 1,
};

struct spot_relocation_ready_completion_t {
    spot_relocation_ready_outcome_t outcome;
};

class spot_relocation_ready_call_t {
public:
    spot_relocation_ready_call_t(spot_relocation_ready_call_t &&) noexcept;
    spot_relocation_ready_call_t(const spot_relocation_ready_call_t &) = delete;
    void defer();
};

template <typename TActor>
class spot_t {
public:
    using actor_type = TActor;

    virtual ~spot_t() = default;
    virtual spot_context_t &context() noexcept = 0;
    virtual const spot_context_t &context() const noexcept = 0;
    virtual void configure() = 0;
    virtual task_t<spot_create_response_t> on_create(
      const message_t &request);
    virtual task_t<void> on_initialize();
    virtual task_t<void> on_closing(
      const spot_closing_context_t &context,
      std::stop_token cleanup_cancellation);
    virtual task_t<void> on_relocation_ready_completed(
      const spot_relocation_ready_completion_t &completion);
    virtual task_t<spot_actor_join_result_t> on_actor_join(
      std::string_view actor_id,
      const message_t &request) = 0;
    virtual task_t<void> on_actor_joined(TActor &actor) = 0;
    virtual task_t<void> on_leave_actor(TActor &actor) = 0;
    virtual task_t<void> on_disconnect_actor(TActor &actor);
};

template <typename TActor>
class entry_spot_t {
public:
    using actor_type = TActor;

    virtual ~entry_spot_t() = default;
    virtual entry_spot_context_t &context() noexcept = 0;
    virtual const entry_spot_context_t &context() const noexcept = 0;
    virtual void configure() = 0;
    virtual task_t<void> on_initialize();
    virtual task_t<void> on_closing(
      const spot_closing_context_t &context,
      std::stop_token cleanup_cancellation);
    virtual task_t<actor_create_response_t> on_create_actor(
      TActor &actor,
      const message_t &create_request);
    virtual task_t<spot_actor_join_result_t> on_actor_join(
      std::string_view actor_id,
      const message_t &request) = 0;
    virtual task_t<void> on_actor_joined(TActor &actor) = 0;
    virtual task_t<void> on_leave_actor(TActor &actor) = 0;
    virtual task_t<void> on_disconnect_actor(TActor &actor);
};

class instance_spot_t {
public:
    virtual ~instance_spot_t() = default;
    virtual instance_spot_context_t &context() noexcept = 0;
    virtual const instance_spot_context_t &context() const noexcept = 0;
    virtual void configure() = 0;
    virtual task_t<void> on_initialize();
    virtual task_t<void> on_closing(
      const spot_closing_context_t &context,
      std::stop_token cleanup_cancellation);
};

class spot_common_context_t {
public:
    std::string_view mesh_name() const;
    node_rid_t node_rid() const;
    spot_id_t spot_id() const;
    std::uint64_t object_generation() const noexcept;
    channel_client_t outbound() const;

    template <typename TCommand>
    spot_send_call_t send_to_spot(spot_id_t target, TCommand command);

    template <typename TRequest>
    spot_request_call_t request_to_spot(
      spot_id_t target,
      TRequest request);

    template <typename TEvent>
    publish_call_t publish(
      std::string channel_name,
      std::string topic,
      TEvent event);

    template <typename THandler>
    timer_t add_timer(std::string name,
      std::chrono::milliseconds period,
      timer_options_t options = {});

    template <typename TWork>
    auto run_cpu_worker(TWork work);

    template <typename TWork>
    auto run_io_worker(TWork work);

};

class spot_context_t : public spot_common_context_t {
public:
    ~spot_context_t();
    spot_context_t(spot_context_t &&) noexcept;
    spot_context_t &operator=(spot_context_t &&) = delete;
    spot_context_t(const spot_context_t &) = delete;
    spot_context_t &operator=(const spot_context_t &) = delete;

    spot_handler_registry_t handlers();
    spot_relocation_ready_call_t relocation_ready();

    template <typename TActor>
    task_t<void> leave_actor(TActor &actor);

    task_t<bool> close();
};

class entry_spot_context_t : public spot_common_context_t {
public:
    ~entry_spot_context_t();
    entry_spot_context_t(entry_spot_context_t &&) noexcept;
    entry_spot_context_t &operator=(entry_spot_context_t &&) = delete;
    entry_spot_context_t(const entry_spot_context_t &) = delete;
    entry_spot_context_t &operator=(const entry_spot_context_t &) = delete;
    spot_handler_registry_t handlers();

    template <typename TActor>
    task_t<void> destroy_actor(TActor &actor);

    task_t<void> destroy_actor(const actor_ref_t &actor);
};

class instance_spot_context_t : public spot_common_context_t {
public:
    ~instance_spot_context_t();
    instance_spot_context_t(instance_spot_context_t &&) noexcept;
    instance_spot_context_t &operator=(instance_spot_context_t &&) = delete;
    instance_spot_context_t(const instance_spot_context_t &) = delete;
    instance_spot_context_t &operator=(const instance_spot_context_t &) = delete;

    instance_spot_handler_registry_t handlers();
    task_t<bool> close();
};

struct spot_actor_join_result_t {
    bool accepted = false;
    std::optional<zlink::framework::message_t> reply;

    static spot_actor_join_result_t accept(
      std::optional<message_t> reply = std::nullopt);

    template <typename TReply>
    static spot_actor_join_result_t accept(TReply reply);

    static spot_actor_join_result_t reject(
      std::optional<message_t> reply = std::nullopt);

    template <typename TReply>
    static spot_actor_join_result_t reject(TReply reply);
};

struct actor_create_response_t {
    bool accepted = true;
    std::optional<zlink::framework::message_t> reply;

    static actor_create_response_t accept(
      std::optional<message_t> reply = std::nullopt);

    template <typename TReply>
    static actor_create_response_t accept(TReply reply);

    static actor_create_response_t reject(
      std::optional<message_t> reply = std::nullopt);

    template <typename TReply>
    static actor_create_response_t reject(TReply reply);
};

enum class spot_create_state_t {
    existing = 0,
    created = 1,
    rejected = 2
};

struct spot_create_response_t {
    bool accepted = true;
    std::optional<zlink::framework::message_t> reply;

    static spot_create_response_t accept(
      std::optional<message_t> reply = std::nullopt);

    template <typename TReply>
    static spot_create_response_t accept(TReply reply);

    static spot_create_response_t reject(
      std::optional<message_t> reply = std::nullopt);

    template <typename TReply>
    static spot_create_response_t reject(TReply reply);
};

struct spot_create_result_t {
    spot_ref_t spot;
    spot_create_state_t state = spot_create_state_t::created;
    std::optional<zlink::framework::message_t> reply;
};

class spot_handler_registry_t {
public:
    template <auto Method>
    spot_handler_registry_t &add_handler(std::string packet_name = {});

    template <auto Method>
    spot_handler_registry_t &add_subscribe(
      std::string channel_name,
      std::string topic);

    template <auto Method>
    spot_handler_registry_t &add_actor_send(std::string packet_name = {});

    template <auto Method>
    spot_handler_registry_t &add_actor_request(std::string packet_name = {});
};

class instance_spot_handler_registry_t {
public:
    template <auto Method>
    instance_spot_handler_registry_t &add_handler(
      std::string packet_name = {});
};

class bound_session_t {
public:
    template <typename TMessage>
    bound_session_send_call_t send(const TMessage &message);

    task_t<void> disconnect();
};

class actor_join_call_t {
public:
    actor_join_call_t(actor_join_call_t &&) noexcept;
    actor_join_call_t &operator=(actor_join_call_t &&) noexcept;
    actor_join_call_t(const actor_join_call_t &) = delete;
    actor_join_call_t &operator=(const actor_join_call_t &) = delete;

    actor_join_call_t &timeout(std::chrono::milliseconds timeout);

    // registers the Join to start once the current handler ends normally.
    void defer();
};

class actor_context_t {
public:
    const actor_ref_t &actor_ref() const noexcept;
    const actor_id_t &actor_id() const noexcept;
    std::uint64_t object_generation() const noexcept;
    std::string_view mesh_name() const noexcept;
    std::optional<spot_id_t> spot_id() const;
    bound_session_t bound_session() const;

    actor_join_call_t join_spot(spot_id_t spot_id);

    actor_join_call_t join_spot(spot_id_t spot_id,
      const zlink::framework::message_t &request);

    actor_join_call_t join_entry_spot();

    actor_join_call_t join_entry_spot(
      const zlink::framework::message_t &request);

    template <typename TRequest>
    actor_join_call_t join_spot(spot_id_t spot_id,
      const TRequest &request);

    template <typename TRequest>
    actor_join_call_t join_entry_spot(const TRequest &request);
};

} // namespace zlink::framework

spot_context_t::publish(...) takes the target ChannelName and topic together. Publish is submitted once per remote MeshNode through the MeshNode ROUTER, and the receiving node only checks the node-local subscription. Each remote ROUTER and local mailbox admits per target, and one target's failure doesn't cancel a previously accepted transmission. Spot/Actor registration belongs to the owner mesh_node_builder_t.

mesh_node_socket_config_t doesn't provide a Framework-level message-size setting for RouteMesh SS. A sender or receiver doesn't reject a message because of a Framework-level complete-message cap. Transport and service-wire representation bounds, HWM, and mailbox budgets remain separate resource and wire guards. If send_timeout isn't specified, the framework default of 1 second is used. If receive_timeout isn't specified, there's no separate bound on receive waiting. HWM must be 0 or greater.

The Spot Actor Join/Relocation-related interface is also a formal contract recorded in this document, and its behavioral meaning follows the common spec. If an implementation or contract test differs from this signature, it's treated as a contract mismatch. join_entry_spot(...) doesn't take a target node RID — the Framework selects the current eligible Entry Spot.

spot_close_reason_t's values are explicit_close=0, host_shutdown=1, relocation_out=2, idle_evicted=3. idle_evicted is an Instance Spot-only reason and isn't delivered to Entry Spot or User Spot. The idle judgment condition and the reactivation rule after cleanup are owned by Spot Model §6.2. The context's deadline is the closing operation's absolute UTC time. The Framework doesn't request stop on cleanup_cancellation before the callback invocation, and requests it once the deadline ends. Only Entry/User/Instance Spot receive the callback, and a per-Actor closing callback isn't provided. Host Shutdown runs the callback while Actor membership and the local instance are still valid, and cleans up scope and authority after completion. Standalone Actor relocation doesn't close the Entry Spot, so it doesn't call this callback.

entry_spot_context_t::destroy_actor(...) is only called from an Entry Spot. An Actor on a user Spot must first complete leave_actor(...) or an Entry Spot join. Since Destroy isn't a membership move, it doesn't call on_leave_actor again, and a duplicate destroy of the same Actor instance doesn't run an additional lifecycle callback and ends as success. The full order follows Actor Model §6.

The cross-node materialization behavior of an Actor and a User/Instance Spot is decided by the factory builder wired to the factory registration. A separate relocation adapter registry or per-operation adapter isn't provided. Only a Spot factory that selected preserve_state_with<TAdapter>() captures/restores Spot application state with spot_relocation_adapter_t<TSpot>. A whole User Spot relocation uses a Spot adapter for the Spot root and actor_relocation_adapter_t<TActor> for each Actor participant. A same-node operation and disable_relocation()/ recreate_on_relocation() don't call the Spot adapter. A cross-node operation that selected disable_relocation() is rejected before capture.

A Spot adapter's capture(...) result has no relocation-adapter-specific size cap, and an empty vector is valid. The Framework doesn't record the payload in the Relocation Store; it splits the payload in source memory into chunks of at most relocation_payload_chunk_limit_bytes and sends them directly over the source–target ordered mesh connection. Ownership of the returned vector moves to the Framework, and the vector passed to restore(...) is owned by that async call. If capture throws or ends as a failed task, admission is restored after durable abort and source normalization. A failed restore's instance is discarded, and the same immutable payload is applied to the instance the new attempt's factory built. If the Framework cancels a callback due to the operation deadline, it's classified as deadline_exceeded. Because recovery can call both methods at least once and they can overlap between a stale attempt and its successor, the adapter must be retry-safe. The Framework doesn't guarantee exactly-once for the adapter's external side effect.

In C++, an ordinary Spot packet and Actor payload handler is registered by spot_context_t::handlers(). An Actor handler is a member of the containing Spot, uses the called Spot instance as this, and takes a mutable Actor, a read-only message_context_t, and a payload as arguments. A message that changes a different Spot's state is submitted through a global spot_id_t direct call. Actor lifecycle isn't a registry registration surface. Since a separate Actor handler object isn't made, per-Actor mutable state is owned by the Actor. A Spot member function of an Entry Spot or PerActor User Spot can be called concurrently for different Actors, and per-Actor state must not be stored in a Spot field. Per-Actor execution resource belongs to Actor activation, is kept on a same-node Join, and is rebuilt at target activation after cross-node Join and relocation. A timer handler, a separate class, is built once per Spot activation and reused. Its constructor dependency parameters are also resolved in the same Spot activation scope. On Spot close and source relocation, the handler and scope are cleaned up. At target activation, a new handler and scope are built. The application doesn't separately register a timer handler as a singleton/scoped/transient service or choose its lifetime. A public handler lifetime option isn't provided. A User Spot and Entry Spot return accept or reject in on_actor_join(...), which takes the actor ID and join request. After commit, the callback directly receives the concrete Actor reference the matching factory built. So a separate membership DTO isn't inserted into the lifecycle callback. The Joined, leave, and disconnect callbacks return task_t<void>, and the lifecycle callback is considered complete only once the task completes. Using yield() to wait for a channel round trip inside a SpotWide User Spot's or Instance Spot's callback returns the shared Spot turn, and resumes the callback with a new turn on the same Spot execution queue after the response. yield() can't be used in an Entry Spot or a PerActor User Spot. An ordinary Spot type must inherit zlink::framework::spot_t<TActor> specifying the concrete Actor type, and an Entry Spot type must inherit zlink::framework::entry_spot_t<TActor>. The two base classes fix the virtual contract of the lifecycle callback, and add_spot<TSpot>() and add_entry_spot<TEntrySpot>() confirm this contract at compile time. The role isn't inferred from name, file location, or method presence alone.

An Instance Spot inherits instance_spot_t and has no Actor callback. The application instance exposes the instance_spot_context_t the Framework wired in at creation through context(). The Framework uses configure() with no argument, on_initialize() with no message, and on_closing(context, cleanup_cancellation) as the actor-free lifecycle. Only a direct packet and timer handler can be registered in configure(...). The instance context's dedicated registry has no Actor handler or Logical Multicast subscription registration member. Duplicate-registering the same stable instance_spot_type or the same Spot class in both a User Spot factory and an Instance factory on the same MeshNode fails as a configuration error before socket bind.

On spot_send_call_t and spot_request_call_t, instance_spot() is the marker without a type, and instance_spot(std::string stable_type) specifies the type. in_mesh(std::string mesh_name) represents the Mesh input. The send terminal is async()task_t<void>; request terminals are async<TReply>() and yield<TReply>()task_t<TReply>. Channel Messaging owns the exact call members.

Spot address messaging §4 owns type/Mesh selection, the creation sequence, and first-message preservation for cold activation. Completion boundaries follow Spot address messaging §5.

Close calls on_closing(context, cleanup_cancellation) once and releases only the location row that satisfies the fencing condition.

A SpotWide User Spot's and its member Actors' relocation is handled as a generic aggregate. It doesn't block host relocation just because active membership exists, and switches aggregate owner and membership together in one commit. A PerActor User Spot switches Spot authority first and moves each Actor separately. spot_context_t::close() and instance_spot_context_t::close() use the current SpotRef the context holds.

An ordinary User Spot close ends with false if even one active Actor membership exists, and keeps admission and authority. The caller can close only after completing leave or destroy of member Actors, and the Framework doesn't hide-move or remove an Actor for close. In host relocation, a SpotWide User Spot moves every current member Actor as one aggregate, and doesn't put a fixed cap on the total participant count. A PerActor User Spot only rebuilds the stateless shell at the new target and moves the Actor as an independent unit.

Object lifecycle §3 owns the scope of stored creation intent resumption and the distinction from steady Ready owner failure.

spot_ref_t is an immutable location snapshot holding the global SpotId, an ObjectGeneration in the 1..9223372036854775807 range, and the MeshName/NodeRid at lookup time. It isn't used as an ordinary message target, and a separate handle, resolver, and address type aren't provided. SpotId and stable type are UTF-8 1..255-byte values, and don't apply trim, case folding, or Unicode normalization. spot_id_t is a case-sensitive std::string of UTF-8 encoded size 1..255 bytes.

class player_actor_t;

class bingo_room_spot_t : public zlink::framework::spot_t<player_actor_t>,
                          public bingo_room_t {
public:
    explicit bingo_room_spot_t(
      zlink::framework::spot_context_t context)
      : context_(std::move(context)) {}

    zlink::framework::spot_context_t &context() noexcept override {
        return context_;
    }

    const zlink::framework::spot_context_t &context()
      const noexcept override {
        return context_;
    }

    void configure() override;

    zlink::framework::task_t<zlink::framework::spot_actor_join_result_t>
    on_actor_join(
      std::string_view actor_id,
      const zlink::framework::message_t &request) override;

    zlink::framework::task_t<void> on_actor_joined(
      player_actor_t &actor) override;

    zlink::framework::task_t<void> on_leave_actor(
      player_actor_t &actor) override;

    zlink::framework::task_t<void> on_disconnect_actor(
      player_actor_t &actor) override;

    start_bingo_game_res_t start_game(
      player_actor_t &actor,
      const zlink::framework::message_context_t &message_context,
      const start_bingo_game_req_t &request);

private:
    zlink::framework::spot_context_t context_;
};

class player_actor_t : public zlink::framework::actor_t {
};

class bingo_entry_spot_t
  : public zlink::framework::entry_spot_t<player_actor_t> {
public:
    explicit bingo_entry_spot_t(
      zlink::framework::entry_spot_context_t context)
      : context_(std::move(context)) {}

    zlink::framework::entry_spot_context_t &context() noexcept override {
        return context_;
    }

    const zlink::framework::entry_spot_context_t &context()
      const noexcept override {
        return context_;
    }

    void configure() override;

private:
    zlink::framework::entry_spot_context_t context_;
};

The three Contexts are move-only handles only the Framework can create. The factory moves the passed-in handle into a read-only member of the application object, and returns that handle from context(). Identity can't be created or replaced through default construction, copy, or assignment.

route_mesh_runtime_options_t is a public DI singleton. Looking up an unregistered ChannelName fails as a configuration error. Only the ChannelName weight can be changed while running. The maximum message size can't be changed after startup. Weight is 0 through 10000, defaulting to 100. A value outside the range is a configuration error in both startup config and runtime change. 0 excludes that membership from a new select-one and Logical Multicast remote target.

A Spot and Entry Spot have their lifetime owned by the activation scope. A default-constructible type registers only the type. A type with a constructor dependency, or one whose application must decide how to build it, is registered as a factory overload. The factory is called by the framework when activating a Spot, and the returned instance's lifetime is also managed in the same activation scope.

An ordinary Spot packet member receives a payload and message_context_t, and a subscription member receives a payload and publish_message_context_t. A member handling actor join admission receives std::string_view actor_id and a zlink::framework::message_t request, and returns whether accepted and an optional reply zlink::framework::message_t as spot_actor_join_result_t. Actor type and source/target Spot and node information are only used for framework-internal routing and validation. Only when accepted is true does it commit the actor location to the user Spot and call on_actor_joined(TActor&). If accepted is false, the actor location doesn't change and the post-joined callback also isn't called. A post-commit result is distinguished by callback name. When Maintenance materializes an Actor on the target Entry Spot, Snapshot first completes the Actor adapter's restore(...), and Recreate completes factory materialization without payload restore. It then restores queue/Actor timer, commits Location authority/Entry membership, and starts Actor message processing. Session–Actor binding §8.2 owns relocation route updates for a bound Session. Infrastructure relocation doesn't call target joined, source leave, or a separate relocation callback.

Only an ordinary same-node/remote User/Entry Spot join uses the existing on_actor_join(...), on_actor_joined(...), and source on_leave_actor(...) contract. In a SpotWide User Spot aggregate's or a PerActor User Spot's Actor relocation, the member Actor's membership callback isn't called. A PerActor Spot policy only allows RecreateOnRelocation and doesn't register a Spot adapter. A Spot field and Spot-level schedule aren't moved. Shared state and schedule that must be kept are placed in an external store the application owns, such as Redis, a database, or a service. The target runtime-private shell uses the same public Spot ID and object generation, and isn't exposed to public lookup before Spot authority. After the authority switch, ToSpot, Create, and Join use the target, and ToActor uses the per-Actor current owner. A stale source route is relayed while preserving operation identity, generation, deadline, correlation, and reply route. The source-local 1-second window from Actor queue seal to the one-way cutover submit's success or failure terminal is an operational goal — exceeding it doesn't cancel or roll back the relocation.

spot_context_t::relocation_ready().defer() is only valid in a Spot turn that registered both spot_wide and application_signaled. The Framework delivers a continued completion from the source if it didn't move or aborted before relay-ready was accepted, and a relocated completion from the target if it moved, to on_relocation_ready_completed(...). The default virtual implementation is a no-op. Before callback completion, pending application messages and timers aren't run.

A duplicate defer() in the default framework_managed, per_actor, Entry/Instance Spot, or outside a Spot turn on the same turn fails with invalid_operation before queue mutation. A different Framework operation on the same turn after defer() is the same error. Since a callback can be re-run during recovery, an override must be retry-safe. An actor packet member is declared on the containing Spot, and receives a mutable Actor, message_context_t, and DTO in that order. The containing Spot being called is this in the member function. The actor disconnected callback also receives the same concrete Actor reference. The runtime's private dispatch converts message_t to a DTO, finds the current Spot instance and Actor, and calls the typed member function. The application doesn't receive an invoker, service provider, serializer registry, or descriptor lookup surface. A sample is also considered to have verified framework behavior only if it goes through the public registration and call path. Even in Entry Spot membership state, an actor packet isn't registered as an ordinary Spot packet. It's registered with add_actor_request<Method>() or add_actor_send<Method>() on spot_context_t::handlers(), and the member takes a mutable Actor, message context, and DTO. The whole stream header metadata isn't exposed to the actor handler as is. The user declares an application metadata forwarding policy like options.metadata().allow_session_to_actor("trace-id"), and the framework puts only the allowed key into message_context_t::metadata. A handler looks up a value with metadata.find(...) or metadata.contains(...). values() provides simple iteration, and find(...) and contains(...) keep handler code from being tied directly to a std::map structure. An empty metadata key and a key of only whitespace have ambiguous meaning, so both allowlist methods reject such a key. This policy is a boundary that keeps the stream frame structure and ActorGateway-internal frame from being exposed on the public handler surface.

timer_t is a public handle expressing the lifetime and cancellation of a Framework timer registration, and the callback is submitted to the owner Spot's serial execution queue. An Entry Spot timer also doesn't globally serialize different Entry Spot instances.

Timer backend selection follows Async Execution Policy §5. timer_tick_t only provides common timer dispatch metadata.

The public surface of ActorGateway session relay is session_actor_manager_t, session_actor_t, actor_context_t, and bound_session_t. MeshNode transport metadata isn't exposed on this surface. The actor context's join_spot(...) request is a DTO or zlink::framework::message_t. A JSON DTO uses the default serializer, so a per-message-type codec setting isn't needed. Only a type that can't be expressed in default JSON, such as Protobuf, MessagePack, or a custom binary payload, wires a serializer extension in startup/options, and the work code keeps the same join call. The join result is the accepted/rejected variant delivered to actor_t::on_join_completed(...). Only the accepted value has the actor ref after the move, and both values carry an application reply zlink::framework::message_t. Entry Spot join also uses the same completion type. An overload that omits the request uses an empty message_t. Raw payload processing is handled by the framework-internal invoker, and a separate raw join overload isn't put on the application public actor context.

The call execution surface expresses the common async call contract in C++ coroutine convention. request(...), send(...), join_spot(...), and join_entry_spot(...) return a call object. A one-way call's async() returns a task_t carrying the bounded admission result up to the send timeout. Session Actor relay(...) doesn't build a separate call object and returns a task_t<void> that produces no normal completion value. For a request, async() is the point that waits for reply completion. An ordinary channel request_call_t gathers metadata and request timeout, and send_call_t gathers only metadata, before submit, and hands them to the framework envelope policy at submit time. The typed packet name is decided by the registration descriptor.

Actor Join has a different execution boundary from other messaging calls. defer() doesn't look up a target or start Store I/O — it only registers Join intent and an inactive queue barrier on the current handler. If the handler ends normally, it activates the barrier and starts Join; if the handler fails, the registration is discarded. defer() doesn't return a value, and doesn't provide an async() or yield() terminal. The Join result is later reported through actor_t::on_join_completed(...). The default timeout is 5 seconds, and a specified value must be in the 1..INT_MAX range in milliseconds. The Framework fixes the absolute deadline using the monotonic clock at defer() time.

yield() returns that turn only in a SpotWide User Spot's or Instance Spot's shared turn. In any other context, it completes with invalid_operation without submitting the operation or returning the turn. A Worker call applies the same execution-context restriction. A CPU worker takes synchronous work, and an I/O worker takes work that returns task_t<TResult>. Starting a terminator twice on one call object completes as a protocol error.

auto reply = co_await client
  .request("profile", query) // selects the call target only by ChannelName.
  .async<profile_reply_t>();

use_profile(reply);

The public framework async surface doesn't use std::future. A blocking wait isn't allowed in a handler, timer, STREAM session callback, or actor relay path.

The error kind projects .NET framework's ZLinkFrameworkErrorKind into C++ naming. async() throws framework_exception_t carrying the same information on failure.

3. Timer

enum class timer_overrun_policy_t {
    skip_late_ticks = 0,
    catch_up_bounded = 1,
    delay_next_tick = 2
};

struct timer_options_t {
    timer_overrun_policy_t overrun_policy =
      timer_overrun_policy_t::skip_late_ticks;
    std::uint64_t max_catch_up_ticks = 1;
    bool stop_on_unhandled_exception = false;
};

struct timer_tick_t {
    std::string name;
    std::uint64_t delivery_index = 0;
    std::uint64_t scheduled_index = 0;
    std::chrono::milliseconds period{0};
    std::chrono::milliseconds scheduled_elapsed{0};
    std::chrono::milliseconds started_elapsed{0};
    std::chrono::milliseconds delay{0};
    std::uint64_t skipped_ticks = 0;
};

struct timer_failure_event_t {
    std::string timer_name;
    std::type_index handler_type;
    std::uint64_t delivery_index = 0;
    bool stopped = false;
    std::string message;
};

class timer_t {
public:
    timer_t();
    ~timer_t();
    timer_t(timer_t &&) noexcept;
    timer_t &operator=(timer_t &&) noexcept;
    timer_t(const timer_t &) = default;
    timer_t &operator=(const timer_t &) = default;

    bool is_disposed() const noexcept;
    void cancel() noexcept;
};

When timer options are omitted, overrun_policy defaults to skip_late_ticks and max_catch_up_ticks defaults to 1. max_catch_up_ticks is used and validated in 1..INT_MAX only when overrun_policy == catch_up_bounded. Other policies do not use or validate this value against that range. This rule interprets the existing timer_options_t fields and adds no public member.

Timer registration validation is owned by stage-wrapper §4.1.

A Framework timer is a logical registration belonging to the owner Actor/Spot. On cross-node relocation, timer name, handler type, period, timer_options_t, scheduling cursor, and pending tick at seal time are automatically included in the relocation payload. The application's relocation adapter doesn't capture/restore a timer or re-register it at the target. The target runtime restores the timer from the payload's logical registration. After the source seals the queue, it doesn't dispatch a new tick, and the target only submits the restored pending tick and the next tick to the owner mailbox once restore and authority commit finish and dispatch admission opens.

4. SPOT Surface

class spot_create_call_t {
public:
    spot_create_call_t(spot_create_call_t &&) noexcept;
    spot_create_call_t &operator=(spot_create_call_t &&) noexcept;
    spot_create_call_t(const spot_create_call_t &) = delete;
    spot_create_call_t &operator=(const spot_create_call_t &) = delete;

    spot_create_call_t &in_mesh(std::string mesh_name);
    spot_create_call_t &creation_request(message_t request);

    template <typename TRequest>
    spot_create_call_t &creation_request(TRequest request);

    spot_create_call_t &timeout(std::chrono::milliseconds timeout);
    task_t<spot_create_result_t> async();
    task_t<spot_create_result_t> yield();
};

class spot_manager_t {
public:
    virtual ~spot_manager_t() = default;
    virtual spot_create_call_t create(std::string stable_type) = 0;
    virtual spot_create_call_t get_or_create(
      spot_id_t spot_id,
      std::string stable_type) = 0;

    virtual task_t<std::optional<spot_ref_t>> find(spot_id_t spot_id) = 0;
    virtual task_t<bool> close(spot_ref_t spot) = 0;
};

spot_manager_t only creates a User Spot. Create has the Framework generate the global SpotId, and GetOrCreate uses the global SpotId the caller provided. An Instance Spot create/get-or-create member and a kind argument aren't provided. A call option and the async() terminal can each be used only once. If the existing authority is an Instance kind or a different stable type, it's type_mismatch; with no eligible capacity, it's capacity_exceeded. A terminal async() returns the spot_ref_t, the existing/created/rejected state, and the creation callback reply together as one spot_create_result_t.

Find only returns the current Ready User SpotRef and doesn't create one. Instance authority isn't included in the manager's Find result. Close only changes a User Spot's SpotRef. An Instance Spot performs local close with the current SpotRef instance_spot_context_t::close() holds in the context. If there's no matching User Spot incarnation, the manager's Close is false; a different generation is invalid_operation; while moving it's unavailable. A public list, resolver, and handle aren't provided.

5. Public Trace Category

The declarations in this document belong to public trace's spot-instance and actor-relocation category. The common meaning is owned by Spot Address And Messaging and Spot · Actor Membership.

The lifecycle callback's call order is owned by MeshNode §7 — handler composition → creation callback → initialization only if accepted → close once.