docs-internal/engine/rivetkit-core-internals.md
Internal wiring reference for rivetkit-rust/packages/rivetkit-core/. These are facts about the current implementation. For the principles that govern how new code is added, see the root CLAUDE.md layer + fail-by-default sections. For state-mutation semantics, see docs-internal/engine/rivetkit-core-state-management.md.
Actor subsystems are composed into ActorContextInner, not separate managers.
ActorContextInner. Behavior sits in actor/queue.rs impl ActorContext blocks. Do not reintroduce Arc<QueueInner> or a public Queue re-export.ActorContextInner. Behavior sits in actor/connection.rs impl ActorContext blocks. Do not reintroduce Arc<ConnectionManagerInner> or a public ConnectionManager re-export.ActorContextInner. Behavior sits in actor/state.rs impl ActorContext blocks. Do not reintroduce Arc<ActorStateInner> or a public ActorState re-export.ActorContextInner. Behavior sits in actor/schedule.rs impl ActorContext blocks. Do not reintroduce Arc<ScheduleInner> or a public Schedule re-export.ActorContext::broadcast. Do not reintroduce a separate EventBroadcaster subsystem.Runtime actor persistence lives in internal SQLite tables. The _rivet_ table prefix is reserved for RivetKit runtime data and must not be used by user schemas.
_rivet_meta is a tiny bootstrap root, not a general-purpose runtime KV store. It is created before the numbered migration ladder, so schema_version cannot live in a table created by that ladder. Its other key, kv_import_state, deliberately survives clear_imported_storage so startup can detect an interrupted legacy import, clear partial destination rows, and retry safely. Generic text accessors for this table are limited to that migration bookkeeping.
| Table | Contents |
|---|---|
_rivet_meta | Bootstrap and import bookkeeping key-value rows such as schema_version and kv_import_state |
_rivet_actor | Cold actor startup fields such as has_initialized and input |
_rivet_actor_state | Hot serialized user state |
_rivet_schedule_events | Durable scheduled actions |
_rivet_conns / _rivet_conn_state | Hibernatable websocket cold metadata and hot state |
_rivet_runtime | Runtime singletons: last pushed alarm, inspector token, queue next id |
_rivet_queue | Queue messages |
_rivet_wf_kv | TypeScript workflow storage with verbatim packed keys |
_rivet_user_kv | Deprecated user c.kv compatibility storage |
The first wake on the migrated runtime runs actor/migrate_kv_to_sqlite/ before actor readiness. The importer live-scans the legacy actor KV keyspace, copies recognized records into SQLite, marks _rivet_meta.kv_import_state = done, and leaves legacy KV bytes untouched as a frozen downgrade snapshot.
Legacy traces under [7, 1, ...] are intentionally skipped and are not copied to _rivet_user_kv. The only runtime KV write after migration is the inspector token mirror at [3], kept until the dashboard stops fetching the token through the public actor-KV endpoint.
request_save uses RequestSaveOpts { immediate, max_wait_ms }. NAPI callers use ctx.requestSave({ immediate, maxWaitMs }). Do not use a boolean requestSave or requestSaveWithin.ActorContext::request_save(...) + ActorEvent::SerializeState { reason: Save, .. }.ActorContext::save_state(Vec<StateDelta>) because Sleep/Destroy replies are unit-only. Direct durability must still clear pending save-request flags after a successful write.request_save / save_state(Vec<StateDelta>). Do not reintroduce set_state / mutate_state._rivet_schedule_events, then resync the envoy alarm to the earliest event.on_state_change callbacks fail with actor/state_mutation_reentrant. Use vars or another non-state side channel for callback-run counters.ActorContext::inspector_attach() returning an InspectorAttachGuard plus subscribe_inspector(). Hold the guard for the websocket lifetime so ActorTask can debounce SerializeState { reason: Inspector, .. } off request-save hooks.ActorContext. Queue-specific callbacks carry the current size; connection updates read the context connection count so unconfigured inspectors stay cheap no-ops.Schedule alarm sync is guarded by dirty_since_push. Fresh schedules start dirty, mutations set dirty, and unchanged shutdown syncs must not re-push identical envoy alarms.Option<i64> in _rivet_runtime.last_pushed_alarm. Startup loads it from SQLite and skips identical future alarm pushes.on_request errors become HTTP 500 responses; on_websocket errors become logged 1011 closes. ConnHandle and WebSocket wrappers surface explicit configuration errors through internal try_* helpers.ActorEvent::Action dispatch uses conn: None for alarm-originated work and Some(ConnHandle) for real client connections. Do not synthesize placeholder connections for scheduled actions.ActorContext sleep state. Queue waits, scheduled internal work, disconnect callbacks, and websocket callbacks report activity through ActorContext hooks so the idle timer stays accurate.onDisconnect work runs inside ActorContext::with_disconnect_callback(...) so pending_disconnect_count gates sleep until the async callback finishes.ActorContexts with ActorContext::build(...) so state, queue, and connection managers inherit the actor config before lifecycle startup runs. ActorContext::build(...) must seed owned queue, connection, and sleep config storage from its ActorConfig; do not initialize those fields with ActorConfig::default().actor_instances: SccHashMap<String, ActorInstanceState>. Use entry_async for Active/Stopping transitions.RegistryDispatcher::handle_fetch owns framework HTTP routes /metrics, /inspector/*, /action/*, and /queue/*. TypeScript NAPI callbacks keep action/queue schema validation and queue canPublish.onRequest HTTP fetches bypass maxIncomingMessageSize / maxOutgoingMessageSize. Those message-size guards apply only to /action/* and /queue/* framework routes, not unmatched user onRequest paths.metadata for JSON/CBOR responses so missing metadata stays undefined. Only explicit metadata null serializes as null.ActorContext before factory creation.has_initialized immediately.ready before the driver hook.run in a detached panic-catching task.started.started after the driver hook completes.Two-phase:
SleepGrace fires onSleep immediately and keeps dispatch/save timers live.SleepFinalize gates dispatch, suspends alarms, and runs teardown.Sleep grace fires the actor abort signal on entry and waits for the run handler to exit before finalize.
Finalize:
run task.ActorContext sleep state for the idle window and shutdown-task drains.ActorContext::wait_for_on_state_change_idle(...) before sending final save events so async onStateChange work cannot race durability.sleep_grace_period budget for the destroy phase.wait_for_on_state_change_idle(...) before final saves.Persistence order:
sleep_grace_period_overridden distinguishes an explicit sleep_grace_period from runtime override defaults.EnvoyCallbacks::on_actor_stop_with_completion. The default implementation preserves the old immediate on_actor_stop behavior by auto-completing the stop handle after the callback returns.EnvoyHandle lookups for live actor state read the shared SharedContext.actors mirror keyed by actor id/generation. Blocking back through the envoy task can panic on current-thread Tokio runtimes.futures::future::BoxFuture<'static, ...> plus the shared actor::callbacks::Request and Response wrappers so config and HTTP parsing helpers stay in core for future runtimes.ActorTask test hooks (install_shutdown_cleanup_hook, lifecycle-event/reply hooks) must be actor-scoped and serialized in tests. Parallel cargo test runs will otherwise cross-wire unrelated actors.rivetkit) interopCtx<A> stays a stateless wrapper over rivetkit-core::ActorContext. Actor state lives in the user receive loop. There is no typed vars field. CBOR encode/decode stays at wrapper method boundaries like broadcast and ConnCtx.Ctx<A>::client() builds and caches rivetkit-client from core Envoy client accessors. Keep actor-to-actor client construction in the wrapper, not core.Start<A> wrappers rehydrate each ActorStart.hibernated state blob back onto the ConnHandle before exposing ConnCtx, or conn.state() stops matching the wake snapshot.rivetkit-rust/packages/rivetkit/src/persist.rs owns typed actor-state StateDelta builders. SerializeState/Sleep/Destroy in src/event.rs stay thin reply helpers that reuse those builders instead of open-coding persistence bytes per wrapper.