durability/workflows

Agent turns cross process boundaries. Tool calls may run for minutes, exec blocks may switch frames to other workers, and the orchestrator on top might be a durable workflow runtime that expects deterministic replay. RuntimeEffectController is the durable effect controller boundary around that nondeterministic work; lash-sansio remains the pure state machine underneath it.

Runtime Effect Boundary

lash-sansio::TurnMachine is still a sans-IO state machine: it yields effects and waits for matching responses. lash-core wraps every reply-producing, nondeterministic effect in a scoped effect-controller call. The default InlineEffectHost delegates to the scoped RuntimeEffectLocalExecutor; durable providers run the turn with a handler-scoped ScopedEffectController so the workflow controller and stable turn id are explicit for that run.

That shape is exactly what durable workflow runtimes (Temporal, Restate, similar) want from a library they're embedding. Lash ships a Restate adapter in lash-restate; other workflow runtimes implement the same controller trait. The workflow ledger can key each invocation, persist its result, and replay with the completed effect already in hand while Lash manages the state-machine checkpoint and response application.

Embedders who only need local durability configure InlineEffectHost plus the regular session store. Embedders who need durable in-flight effects create a handler-scoped controller, then pass it through .turn_id(...).effects(&controller).stream_to(&sink) / .run() on the facade. Lower-level runtime adapters can still build explicit ScopedEffectController values for TurnOptions::new(cancel, scope). RestateRuntimeEffectController is the first-party workflow adapter shape.

Runtime Effects

RuntimeEffectKind names the durable effect controller surface. For turn-originated work, the runtime converts the sans-IO Effect into one or more typed controller calls, invokes the active scoped controller, then feeds the matching Response back into TurnMachine.

Runtime effectPayloadOutcome / response
LlmCallLlmRequestSpec with attachment refs, not bytesLlmComplete with Result<LlmResponse, LlmCallError>
DirectRef-only LlmRequestSpec plus a usage_source label, covering both plugin/helper and direct one-shot LLM callsDirect with Result<LlmResponse, LlmCallError>
ToolAttemptOne PendingToolCall from a sans-IO tool-call batch (the batch itself is also recorded as a ToolBatch command)One CompletedToolCall; the runtime reassembles ToolResults for TurnMachine
ProcessProcessCommand::{Start,List,Transfer,DeleteSession,Await,Cancel,Signal} against the explicit process admin planeProcessEffectOutcome with process records, visible observations, transfer completion, session-delete report, await output, cancellation records, or appended signal events
ExecCodecode: StringExecResult with Result<ExecResponse, String>
Checkpointcheckpoint: CheckpointKindCheckpoint with plugin-injected messages
Sleepduration_ms: u64Sleep
SyncExecutionEnvironmentupdate_machine_config flagExecutionEnvironmentSynced with refreshed prompt + tool specs
AwaitEventkey: AwaitEventKey, the durable wait a pending tool completion or process signal parks onAwaitEvent with a Resolution (ok / err / timeout / cancelled)

Fire-and-forget sans-IO effects such as Emit, Log, Progress, and Done remain driver and commit-pipeline responsibilities. They do not cross RuntimeEffectController because they do not ask the host to run nondeterministic work and return a response.

Waiting States

When the machine yields an effect that needs a response, it parks in a MachineState variant that retains exactly the data needed to re-emit the same effect identically after restore. Five variants matter for durability:

WaitingLlm

effect_id, the ref-only LlmRequestSpec, and optional mode driver_state. Restore re-issues the same request (same model, same messages, same tool spec, same attachment refs) under the same EffectId.

WaitingTools

effect_id plus the full Vec<PendingToolCall> batch the model emitted. On restore, the runtime sees the same list with the same call ids, but the effect controller is invoked once per logical tool call so durable providers can persist, dedupe, and resume each call independently.

WaitingExec

effect_id, the original code block, and the mode's driver_state. RLM persists its trajectory cursor plus projected snapshot markers; ref-backed projected globals are rehydrated through the active ProjectionResolver before the same exec re-runs.

WaitingCheckpoint

effect_id, the CheckpointKind being run, and an on_empty resume action: whether to prepare another protocol iteration or finish the turn if the checkpoint plugin contributes no messages.

WaitingExecutionEnvironment

effect_id plus update_machine_config flag. The least state-bearing of the waiting variants. Restore simply re-asks the host for the live tool catalog.

Non-waiting

PreparingProtocol, PrepareIteration, and Finished hold no outstanding effect. Restoring into these is a no-op for re-emission; the machine just resumes from the persisted message and event log.

Invocation Metadata

EffectId(u64) is a monotonic counter held in the machine. The next id is allocated on each yielded effect. The first effect of a turn is always EffectId(1); the n-th effect is always EffectId(n) for that deterministic run. Durable workflow hosts replay the handler and return recorded effect outcomes for the same scoped replay keys.

Every controller invocation receives a serializable RuntimeInvocation: scope identifies the session and optional turn coordinates, subject identifies the effect, caused_by carries the typed causal parent, and replay.key provides the stable replay discriminator. The live RuntimeEffectEnvelope is serializable and carries ref-only request data. Cancellation, streams, provider trace sinks, and attachment byte hydration stay in the local executor or host workflow.

The built turn replay key includes session_id, turn_id, turn_index, protocol_iteration, effect kind, and EffectId; per-tool invocations append the model tool call id. Durable tool steps derive child replay keys from the caller's invocation plus the prepared tool call id and step_id; the stable envelope hash also includes the JSON input. Scoped durable runs require a non-empty stable turn_id. Direct completions use a framed identity over session, optional turn, usage source, typed causal parent, and either an explicit replay key or a deterministic per-lane ordinal. Request content is deliberately excluded: unkeyed call order must be deterministic under redrive, and conditional or concurrent branches need stable explicit keys.

EffectId is local to one turn. Across turns, the counter resets to 1; use the full replay key, not the bare id, for globally unique effect identity.

Checkpoint Primitive

The sans-IO turn machine exposes a serializable checkpoint primitive:

impl<M: TurnProtocol> TurnMachine<M> {
    pub fn checkpoint(&self) -> TurnCheckpoint<M>;

    pub fn restore_from_checkpoint(
        config: TurnMachineConfig<M>,
        checkpoint: TurnCheckpoint<M>,
    ) -> Self;
}

TurnCheckpoint<M> is fully serde-compatible. Fields:

restore_from_checkpoint remains a sans-IO machine primitive for tests and protocol work. Runtime crash recovery does not persist or reload in-flight machine payloads from the store. Local/inline runs reopen the last committed session state; durable workflow integrations replay the host handler and effect history, then Lash retries the final commit.

Replay Safety

Workflow replay re-enters the normal handler and returns recorded outcomes for previously completed effects. Lash does not store a separate in-flight effect history. From the workflow's perspective, the effect host history is the durable activity log; the Lash store remains the committed-state log.

Driving Effects From A Workflow

The runtime turn driver still runs the TurnMachine poll/response loop. A workflow integration wraps the effect boundary instead of reimplementing that loop:

#[async_trait::async_trait]
impl RuntimeEffectController for WorkflowEffectController {
    async fn execute_effect(
        &self,
        envelope: RuntimeEffectEnvelope,
        local_executor: RuntimeEffectLocalExecutor<'_>,
    ) -> Result<RuntimeEffectOutcome, RuntimeEffectControllerError> {
        let key = envelope.invocation.replay_key().unwrap_or("runtime-effect").to_string();
        self.workflow
            .run_activity(key, envelope.invocation.clone(), || async move {
                local_executor.execute(envelope).await
            })
            .await
    }
}

The shape matches Temporal's workflow + activity split and Restate's handler + side-effect split. Each typed runtime effect can become one durable activity: the invocation carries typed scope, subject, causal parent, and replay key, and replay.key joins the activity result back to the logical turn effect. Restate immediately awaits concrete ctx.run(...) calls around the normal local executor for recorded non-sleep effects, uses Restate timers for sleeps, and keeps direct Restate primitives for workflow calls and awaited events. The store is touched only when Lash commits settled session state.

Deterministic Lash/Restate contract failures are terminal handler errors. A recorded effect whose replay hash no longer matches the current envelope, or a recorded Restate run that completed with a terminal failure, returns an explicit terminal error instead of panicking into an infinite backing-off invocation. Hosts should record a failed turn/event and clear their UI running state when the workflow reports such a terminal failure.

Restate hosts also need durable invocation identity outside the handler. RestateIngressClient submits workflow/service/object /send requests and parses the response body's invocationId. Foreground-turn Stop uses RestateTurnDeployment::turn_work_driver(), which addresses Lash's reserved keyed promises by session and turn id and can attach to the committed terminal evidence. RestateAdminClient invocation cancel/kill remains engine-level break-glass and must never be reported as a Lash Cancelled result.

Process effects cross the same controller boundary for start/list/transfer/delete-session/await/cancel/signal, but durable execution of a background process is owned by the process work driver and worker, not by the turn that happened to start it. The registry stores durable state; waits live on the driver/awaiter seam. The process section below covers the durable intent record, observer edges, worker rebuild inputs, Restate workflow metadata, and event-sourced terminal state.

Successful completion uses RuntimeCommit.turn_commit. Ordinary turn commits accept identical retries for the same (session_id, turn_id) and reject a changed commit hash. Session-graph appends add a versioned semantic request identity: an identity match returns the stored first result without applying any of the retry envelope, while changed content at the same version is a typed caller conflict and an encoding-version mismatch falls back to exact-hash semantics. A workflow replay can therefore retry the final Lash commit without duplicating or mutating settled state.

Tool attempts are atomic journal entries. Host code inside an attempt is opaque, including direct completions and retry delays; it does not borrow the controller to create nested durable steps. A completed attempt replays its recorded result, while an attempt retry or crash-before-record runs its in-attempt effects again. Work that needs multiple independently durable effects or waits is decomposed into process steps.

Processes

A process is durable background work named by a ProcessRecord. Process support is opt-in: a deployment installs a ProcessRegistry (such as lash-sqlite-store::SqliteProcessRegistry or lash-postgres-store::PostgresProcessRegistry) and either lets the facade build the inline ProcessWorkDriver or supplies an external one. The registry records state; the driver owns execution and coordination waits. Without a process plane, start/await/cancel/transfer/list all fail clearly rather than degrading silently.

Process lifecycle
flowchart LR Start["start <tool> / start name(...)"] --> Register["register_process
ProcessRecord"] Register --> Observe["add_observer
weak session edge"] Register --> Worker["DurableProcessWorker
(ephemeral runtime from captured env)"] Worker --> Events["append_event
(event-sourced state)"] Events --> Terminal["keyed terminal event"] Observe --> Await["await handle
ProcessWorkDriver / ProcessAwaiter"] Observe --> Cancel["cancel observed handle
process.cancel_requested"] Terminal --> Await

A process is self-contained: the record carries its ProcessOriginator (host or session) as pure provenance and a captured execution-environment reference. Wake subscription and session observation are indexed edge state, not lifecycle fields. Deleting a session removes its weak observer rows, clears its subscriptions, and discards its pending wake deliveries without changing process records or event logs; it never cancels a process. See docs/adr/0011-self-contained-processes.md and docs/adr/0046-process-transitions-are-events-record-is-a-fold.md.

Records are event-sourced: terminal state and cancellation are keyed process events on the record (process.cancel_requested routes to the workflow's shared cancel handler), not mutable state fields. Tool and Lashlang process rows carry a captured execution-environment reference (a content-addressed process_execution_env artifact holding only Process Plugin Options and policy), and workers instantiate an ephemeral runtime from that immutable environment plus host-owned worker config; they never rebuild the originating session. Durable tool authority snapshots live behind plugin-owned options, not as ad hoc process-environment metadata. A Restate-backed start stores external_ref.backend == "restate" with external_ref.id == "LashProcessWorkflow/{process_id}". Wake semantics write durable wake-inbox rows only when the process has a wake target; a process without one still records wake events, but they deliver nowhere. Checkpoints drain inbox rows beside in-memory turn injections.

Waiting is deliberately not a registry capability (docs/adr/0016-process-waits-live-on-the-work-driver-seam.md). Store-backed registries expose point reads and writes only. The facade wraps inline registries with a ProcessChangeHub so local mutations wake ProcessAwaiter waiters, and the awaiter falls back to narrow point-read backoff when another process may be changing the store. External drivers can attach terminal awaits to their own durable promise; Restate attaches host awaits to LashProcessWorkflow/{process_id}/await_terminal.

Lashlang process starts validate before registration in the Lashlang runtime path: it rebuilds a candidate plugin session and Tool Catalog from the captured Process Plugin Options, constructs the candidate LashlangHostEnvironment, and checks it satisfies the target artifact's recorded Host Requirements before writing ProcessInput::Engine { kind: "lashlang", payload }. The registered ProcessEngine repeats the Host Requirements check during execution and recovery, so a recovered row cannot run against a drifted or incompatible rebuilt environment.

ProcessInputStarted by
ToolCallGeneric start <tool> wraps a prepared foreground tool call and grants a tool-labeled handle. Foreground tool calls run normally and cannot self-register durable processes.
Engine { kind: "lashlang" }A named Lashlang process launched with start name(...). The payload stores only module_ref, process_ref, host_requirements_ref, display name, and JSON args. The module artifact is loaded from the Lashlang artifact store before row registration and again before worker compilation; missing artifacts, missing snapshot-backed plugin options, or incompatible surfaces fail at the validation boundary.
SessionTurnA managed session turn run as a process: the subagent (spawn_agent) path.
ExternalBackground work accepted from outside the local process, carrying opaque backend metadata.

Turn / Process Symmetry

A background process can be started by a turn or by a matched trigger occurrence, and however it starts it is identically durable. Start writes durable intent; a lash-owned DurableProcessWorker owns execution and recovery for every non-terminal process. Timers and recurring jobs live on the host side as source owners; when they emit an occurrence with a stored source key, Lash starts matched target processes through the same durable worker path.

Turn in-flight replay is effect-host owned. A process is store-backed background work: it is fenced by a ProcessLease, resumes from registry events, and recovers through DurableProcessWorker::drive_pending_processes. Process leases carry owner id, lease token, fencing token, claim time, and expiry; stale process writers fail before committing durable state.

A turn supplies its per-invocation durable controller through ScopedEffectController. Out-of-turn starts (trigger deliveries, facade start, and recovery sweeps) write durable registry intent and execute through the worker's wired controller. A durable host never silently re-runs a process on an inline controller.

User-facing cancellation is intentionally separate from execution recovery. Session tool surfaces validate observer visibility before calling the scoped ProcessService cancellation operation; cancel-all lists live visible processes and cancels each directly. There is no host-injectable cancellation policy in the runtime.

The recovery sweep is disposition-driven, not a re-run-everything loop (docs/adr/0019-process-recovery-obeys-declared-disposition.md): every registration declares a required RecoveryDisposition and the sweep applies it mechanically. It lists non-terminal processes, claims each available lease, and skips work held by another live owner; then a Rerunnable row is re-run and completed exactly as before, a started OwnerBound row remains non-terminal unless owner drain or a lapsed-lease Abandon Request authorizes terminalization (a not-yet-started one stays claimable by any peer, since a first execution is not a re-execution), and an ExternallyOwned row is never claimed or executed. Lease expiry alone never terminalizes. Abandoned is a fourth terminal state, peer to Completed | Failed | Cancelled, carrying the writer (owner drain, reconciled request, or exhausted execution engine) and the evidence that licensed it. The sweep is idempotent by persisted process_id; the Restate invocation is keyed as LashProcessWorkflow/{process_id}. The full verdict table and the crash-vs-drain paths are in running in production.

A subagent spawn is itself such a process. agents.spawn builds a SessionCreateRequest plus a TurnInput and emits one ProcessInput::SessionTurn, so a spawned child runs through the same generic process path (and therefore inherits provider re-supply, durability, and recovery) rather than a bespoke child-creation route. Recursion safety (the subagent depth ceiling and tool-hiding at that ceiling) and the submit_error terminal are carried as request config, not lost.

Replay Ownership, Not a Durability Claim

The runtime derives effect dispatch from the controller the host wired. EffectReplayOwnership::Runtime means Lash executes and records effects locally; Controller means the controller owns replay and Lash dispatches through it.

This is a mechanical fact, not an end-to-end guarantee. Store, registry, attachment, and artifact traits do not advertise a shared durability tier, and the runtime does not compare independently wired facets to infer deployment guarantees. Hosts establish those guarantees by choosing persistent substrates and validating them with the public conformance suites.

The inline host defaults to runtime-owned replay. SQLite, PostgreSQL, and Restate effect hosts explicitly report controller-owned replay; scoped and task controllers preserve that ownership. Restate effect hosts always require an ingress connection, and SQLite effect hosts are always file-backed, so neither concrete host has a constructor that contradicts its replay behavior.

What's Covered, What Isn't

The effect-controller seam covers nondeterministic work that Lash can name as one runtime effect. Some workloads still sit outside it:

Inside the seam

Turn LLM calls, atomic native tool attempts, RLM exec, plugin checkpoints, execution-surface sync, controller-level retry sleeps, independent direct completions, direct LLM completions, and controller-mediated process start/list/transfer/delete-session/await/cancel/signal. Turn effects include EffectId and replay key. A direct completion inside a tool attempt belongs to that attempt rather than crossing the controller separately.

Outside the seam

Fire-and-forget Emit and Log effects, and the individual side effects inside opaque tool code. Those effects inherit the tool attempt's at-least-once execution semantics. Workflows that need separate durable boundaries decompose them into process steps.

Sessions opened through the regular LashCore / LashSession path also get durability through the session store, but at a coarser grain: a turn either commits to the store atomically or fails as a whole. Run with a scoped RuntimeEffectController when you need crash recovery inside a turn: when a single LLM call is expensive enough to replay, or when an individual tool call can run for minutes and you can't afford to lose its progress.

Test Coverage

The durability properties above are pinned by unit and integration tests across the runtime host, sans-IO machine, and protocol drivers:

TestAsserts
scoped_borrowed_effect_controller_uses_required_stable_turn_idA stack-borrowed controller can drive a turn through ScopedEffectController, and empty scoped turn ids are rejected.
checkpoint_before_llm_completion_reissues_same_logical_llm_callRestore from a checkpoint taken mid-LLM yields a fresh LlmCall effect with the same EffectId and identical request payload.
checkpoint_after_llm_result_replays_checkpoint_without_second_llmRestore after the LLM completed but before the checkpoint plugin ran yields the pending Checkpoint effect, with no duplicate model call.
tool_call_effect_crosses_controller_per_logical_call_and_runs_local_toolsOne sans-IO tool batch becomes one controller invocation per logical tool call, with call ids in the replay keys.
turn_effect_envelope_does_not_carry_checkpoint_payloadRuntime effect envelopes carry stable invocation identity and replay keys without embedding store-owned in-flight payloads.
in_turn_direct_completion_uses_effect_controller_without_out_of_band_commitDirect completions cross the active scoped controller and do not commit state through a hidden side channel.
processes_fails_loudly_when_process_registry_is_unavailableProcess validation, non-empty transfer, and cleanup fail clearly without an installed ProcessRegistry, while empty transfer remains an explicit no-op.
lashlang_process_uses_artifact_refs_for_nested_startsA durable Lashlang process with ref-only input can reload its ModuleArtifact, compile the selected process_ref, and run nested start/await without storing a full module snapshot on the process row.
executor_reports_disabled_lashlang_abilities_at_link_timeDisabled process declarations, starts, process sleep/signals, and triggers parse but fail during link with lashlang feature `...` is disabled by this host, producing no runtime tool calls or observations.
queue_completion_and_turn_commit_stamp_are_atomicA rejected queue completion does not record the final turn stamp; a valid commit records the stamp and identical retries return the recorded result.
final_commit_stamp_is_idempotent_and_conflicts_on_changed_hashThe store accepts identical final commit retries for one (session_id, turn_id) and rejects changed hashes.
runtime_persistence_recovery_lawsFresh handles over one substrate prove durable claim expiry/recovery, checkpoint survival before settlement, atomic commit-plus-settlement, and recorded-commit replay. These are store laws; they do not certify runtime turn phases.
checkpoint_component_refs_survive_cold_reopensThe helper constructs handle B only after dropping body writer A, then constructs reader C only after dropping ref-only writer B; component bodies remain hydratable through their durable refs.
*_effect_host_satisfies_cold_process_await_event_conformanceA helper process registers and owns a real durable wait before it is killed; a fresh owner resolves the promise and another fresh observer reads the same terminal.
*_effect_replay_satisfies_cold_process_crash_conformanceAn external effect killed after execution but before outcome persistence executes once more after takeover, then replays the recorded outcome without a third execution.
append_receipt_mixed_usage_envelope / concurrent_same_append_operation_applies_exactly_once / append_receipt_survives_reopenAppend receipt replay restores interleaved usage for the next natural commit, concurrent first-attempt/retry races append once, and SQLite/PostgreSQL receipts survive reopening.
old_format_append_receipt_returns_public_leaf / append_request_receipt_rejects_corrupt_node_countRaw pre-upgrade receipt JSON returns a real public leaf, and matching receipt evidence with contradictory requested-node counts is rejected as typed corruption.
retry_delay_crosses_effect_controller_as_sleep_effectRuntime-owned retry delays cross the effect controller as Sleep effects instead of sleeping directly.
restate_controller_executes_non_sleep_effect_inside_runThe Restate adapter wraps reply-producing Lash effects in a named ctx.run(...) call instead of executing them directly in the workflow handler.
restate_controller_routes_sleep_only_through_timerRuntime sleep effects map to Restate's durable timer path without using a journaled run closure.
recorded_runtime_effect_hash_mismatch_fails_explicitly / recorded_runtime_effect_hash_match_returns_replayed_outcomeRestate replay validates the recorded effect envelope hash and returns a clear terminal error for deterministic mismatches while accepting matching recorded outcomes.
restate_positional_replay_records_tool_batch_as_one_commandA completed recorded tool-batch command is replayed positionally without re-polling or re-running the local tool executor.
restate_handler_replay_retries_final_lash_commit_idempotentlyAfter terminal output has been produced, handler replay retries the final Lash commit once and relies on the store's idempotent commit stamp instead of duplicating a turn.
restate_ingress_client_parses_send_invocation_id / restate_ingress_client_accepts_previously_accepted_sendThe Restate ingress client parses {"invocationId","status"} from /send and treats both accepted and previously accepted sends as durable invocation identity.
cancel_watch_reissues_after_attach_ceiling_until_segment_completes / non_timeout_cancel_watch_error_fails_the_segmentA cancellation watch re-attaches across repeated transport ceilings without failing a healthy long-running segment, while a non-timeout transport fault remains a handler failure.
exhausted_cancel_confirmation_is_a_retryable_handler_error / absent_event_after_cancel_promise_is_a_terminal_handler_errorTransient cancellation-confirmation read exhaustion stays retryable for the workflow engine, while a resolved promise with no durable cancel event is terminal under the single-primary registry contract.
restate_process_attach_preserves_re_attach_signal_on_ceiling / restate_turn_attach_preserves_re_attach_code_on_ceilingProcess and turn host attach seams preserve a typed ceiling-elapsed signal so callers can re-attach the same durable identity instead of parsing display text.
restate_attach_body_read_is_clamped_to_control_timeout / restate_connection_timeout_config_has_serde_defaultsOnce attach headers arrive, body reads use the tighter control budget; timeout configuration rejects unknown fields and zero values while retaining the 30-second/6-hour defaults.
restate_admin_client_cancels_kills_and_queries_invocation_statusThe Admin client sends graceful cancel, reserves kill for cleanup, queries a specific invocation, and lists unfinished pending, ready, running, backing-off, and suspended service invocations for E2E assertions.
restate_controller_schedules_process_workflow_without_running_executorRestate process starts register a process record, create the requested observer edges, write external_ref.backend == "restate" / LashProcessWorkflow/{process_id} metadata, and do not run the local executor.
restate_controller_schedules_lashlang_process_with_serializable_inputLashlang process registrations cross the Restate workflow start boundary with module/process/host requirements refs, JSON args, display label, and execution context data intact.
restate_controller_cancel_requests_call_workflow_cancelProcess cancellation is routed to the workflow's shared cancel handler while terminal state remains event-sourced from process events.
process_workflow_endpoint_smoke_schedules_runs_and_cancels_processAn in-process endpoint smoke drives controller process start, endpoint-served workflow run, and shared cancel through a recording runner without an external Restate daemon.
sqlite_process_recovery_reopens_registry_worker_observers_wakes_and_cancelA Restate process start runs through RestateCoreProcessRunner against a SQLite registry/session-store pair, then reopens the registry to list observers, await terminal output through the process awaiter/driver seam, drain and ack wake inbox rows, and route cancel through a rebuilt worker.
sqlite_trigger_started_process_recovered_after_worker_registry_reopenA trigger-started (not turn-started) non-terminal process is listed, lease-claimed, and run to its terminal value by a fresh worker over a reopened SQLite registry; the recovery sweep is idempotent by process_id on a second run.
active_process_lease_fences_competing_owner / superseded_process_lease_cannot_renew / renewed_process_lease_survives_original_expiry / completed_lease_releases_and_reclaim_bumps_fencing / stale_lease_completion_cannot_release_live_lease / list_non_terminal_excludes_terminal_processesThe ProcessRegistry conformance suite pins pure state behavior: single-owner fencing, renewal past expiry, fencing-token bumps on reclaim, stale-completion rejection, and the recovery worklist query, run against SqliteProcessRegistry, PostgresProcessRegistry, and TestLocalProcessRegistry.
rlm_spawn_process_handle_returns_child_final_value / spawn_uses_live_parent_provider_when_selecting_subagent_modelA subagent spawn runs through the generic ProcessInput::SessionTurn path and the awaited handle returns the child's final value, with the child selecting its model from the live parent provider: the headline proof that the provider-inheritance bug is dissolved by the collapse, not patched.
process_workflow_binds_to_restate_endpoint_and_discovers_handlersThe generated SDK binding builds with Endpoint::builder().bind(LashProcessWorkflowImpl::new(runner, registry).serve()).build() and advertises workflow run plus shared cancel handlers.
process_workflow_impl_runs_and_cancels_through_runnerThe Restate workflow implementation delegates run to the process runner, writes terminal events through the registry, and records cancel intent before forwarding cancellation.
sqlite_store_schema_excludes_embedded_turn_replay_tablesSQLite persists final turn commit stamps and does not create store-owned in-flight effect replay tables.
checkpoint_preserves_parallel_tool_batch_before_any_resultRound-tripping a WaitingTools state preserves every PendingToolCall with its id and arguments intact.
checkpoint_after_mixed_tool_batch_results_replays_model_feedback_onceMixed success / failure / cancellation tool outcomes round-trip without re-emitting per-tool events to the stream.
checkpoint_round_trips_waiting_exec_driver_stateRLM exec waits round-trip with their driver state intact; the same code block and trajectory cursor resume after restore.
standard_checkpoint_redrives_parallel_tool_batch_without_losing_callsEnd-to-end through a protocol driver: standard-protocol parallel tool batches resume identically after restore.
standard_checkpoint_after_tool_control_finish_preserves_terminal_outcomeA tool that authored a terminal value via tool-control finishes the same way on replay.
rlm_checkpoint_redrives_pending_exec_code_with_driver_stateRLM pending ExecCode re-runs with the same driver state and projected-binding snapshot context intact.
rlm_checkpoint_after_exec_fanout_tool_outputs_preserves_structured_outcomesRLM exec results carrying parallel tool outcomes preserve success / failure / cancellation structure across restore.
rlm_exec_result_does_not_store_tool_call_ids_or_replay_tool_eventsRLM exec outcomes preserve live tool provenance without persisting tool call ids into trajectory history or leaking replayed ToolCallStarted events to the activity log.
read on ·