# Ralph Progress Log
Started: Sun Apr 19 2026

## Codebase Patterns
- `rivetkit-core` startup lives in `ActorTask::start_actor`, and scheduled alarms route through `LifecycleCommand::FireAlarm` into mailbox `ActorEvent::Action`; do not reintroduce `RuntimeCallbacks` or `ActionInvoker`.
- Inspector workflow history/replay flow through `DispatchCommand::WorkflowHistory` / `WorkflowReplay` into `ActorEvent::{WorkflowHistoryRequested, WorkflowReplayRequested}`; workflow-enabled is inferred from mailbox replies (`actor/dropped_reply` = unsupported).
- Mailbox-era runtime-side injections go through `ActorContext::try_send_actor_event`; connection open/close and subscribe gating use `Reply`-backed `ActorEvent`s, not callback tables.
- Save persistence is split: `ActorContext::request_save(...)` only coalesces dirty/immediate intent, `ActorTask` owns the `SaveTick` debounce, and `ActorContext::save_state(Vec<StateDelta>)` handles direct durable writes + save-flag reset (uses `save_state_with_revision` under the hood).
- Transport-only hibernation changes queue pending deltas on `ConnectionManager` and flush through `ActorContext::save_state_with_revision`; do not write KV `[2] + conn_id` outside `ActorState::apply_state_deltas`.
- Wake-time hibernated-connection filtering lives in `ActorTask::settle_hibernated_connections`; tests use `ActorContext::set_hibernated_connection_liveness_override(...)` until `rivet-envoy-client` exposes a real gateway_id/request_id liveness query.
- Sleep/destroy shutdown final state comes from `ActorEvent::Sleep` / `Destroy` delta replies, not a trailing `persist_state(immediate)` call. Destroy disconnects hibernatable connections after the final delta flush and removes their KV entries; sleep leaves them resident for wake.
- `StateDelta::ConnHibernation` bytes are actor-owned payloads; core wraps them with live connection metadata when writing KV `[2] + conn_id`. Transport-only metadata refreshes must reuse the stored payload, not `conn.state()`.
- Actor-owned bounded inbox producers use `try_send_lifecycle_command` / `try_send_dispatch_command` or lifecycle-event `try_reserve`, returning `actor/overloaded` instead of awaiting `mpsc::Sender::send`.
- ActorTask action children must stay concurrent; a per-actor action lock deadlocks unblock/finish actions behind long-running ones.
- State mutations from inside `on_state_change` fail with `actor/state_mutation_reentrant`; tests counting callback runs should use `vars` or another non-state side channel.
- High-churn sleep-readiness changes go through `ActorContext::notify_activity_dirty_or_reset_sleep_timer()` so `ActorTask` receives one coalesced `ActivityDirty` event.
- `ActorConfig` keeps `sleep_grace_period_overridden` separate from `sleep_grace_period` so explicit grace doesn't get confused with the legacy `on_sleep_timeout + wait_until_timeout` fallback.
- `ActorContext::can_sleep()` tests must set both `ready` and `started` before asserting blockers; otherwise it short-circuits to `CanSleep::NotReady`.
- Async sleep-region wrappers on `ActorContext` must use Drop guards so keep-awake counters decrement if the wrapped future is dropped or unwinds.
- RivetError derives in `rivetkit-core` generate JSON artifacts under `rivetkit-rust/engine/artifacts/errors/`; commit new generated files with new error codes.
- Moved `rivetkit-core` tests in `tests/modules/` are compiled through `#[path = ...]` shims, so they must track private API signature changes.
- Actor runtime Prometheus metrics flow through `ActorContext::metrics()` / `ActorMetrics`; use `UserTaskKind` and `StateMutationReason` label helpers.
- Exact `cargo clippy -p rivetkit-core -- -W warnings` checks deny-linted workspace deps; use `--no-deps` to isolate core warnings, but fix dependency deny blockers when the exact command is required.
- After native `rivetkit-core` changes, force-rebuild `@rivetkit/rivetkit-napi` before TS driver tests; the normal N-API build skips when a prebuilt `.node` artifact exists.
- Timing-sensitive RivetKit driver tests run under the package Vitest single-worker config; do not re-enable broad worker fan-out for native runtime sleep/destroy/hibernation filters without a fresh flake loop.
- Sleep/destroy/hibernation driver assertions should reacquire actor handles by key after sleep or shutdown boundaries; resolved direct handles can still point at stopped actor IDs.
- Raw WebSocket close/onDisconnect DB assertions are currently nondeterministic under the native task model; future work should add an explicit close-callback lifecycle acknowledgement before unskipping them.
- TypeScript `onStateChange` fixtures, examples, and docs should keep callbacks read-only against `c.state`; use `vars` for callback counters or derived runtime-only values.
- N-API `ActorContext::set_state` must propagate core errors with `napi_anyhow_error` so TS callbacks observe structured `RivetError` codes like `actor/state_mutation_reentrant`.

## Known out-of-scope gaps (need cross-crate work)
- Envoy-backend `kv.rs::apply_batch` uses sequential `batch_put` + `batch_delete` RPCs instead of one atomic UniversalDB transaction. Breaks the atomicity invariant for every multi-delta flush (SaveTick/Sleep/Destroy/save_state). Fix requires a runner-protocol change.
- `ActorContext::hibernated_connection_is_live` production path is `todo!()`. Any live wake with persisted hibernated conns will panic until `rivet-envoy-client` exposes a gateway_id/request_id liveness query.

---
