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 handle grants, 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, durable tool wait, or process signal parks onAwaitEvent with a Resolution (ok / err / timeout / cancelled)
DurableStepstep_id plus JSON input, emitted by ToolDurableEffects::run_json(...)DurableStep with the recorded JSON value

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 derive stable keys from the request payload, usage source, typed causal parent when present, and optional caller-provided DirectRequest::replay.

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; RestateAdminClient uses Admin HTTP for graceful cancellation, status lookup, and unfinished-invocation introspection. Forceful kill is reserved for test/dev cleanup after graceful cancel fails.

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, handle grants, worker rebuild inputs, Restate workflow metadata, and event-sourced terminal state.

Successful completion uses RuntimeCommit.turn_commit. The store accepts identical retries for the same (session_id, turn_id) and rejects a changed commit hash, so a workflow replay can retry the final Lash commit without duplicating or mutating settled state.

Advanced in-process tools use ToolContext::durable_effects() to borrow this same boundary without receiving workflow-native context. run_json(...) emits DurableStep; external_event_key(...) signs a custom AwaitEventKey; await_event_json(...) waits through AwaitEvent; and emit_process_event(...) appends a domain process event only when the tool is executing inside a process.

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 --> Grant["grant_handle
per-session handle"] Register --> Worker["DurableProcessWorker
(ephemeral runtime from captured env)"] Worker --> Events["append_event
(event-sourced state)"] Events --> Terminal["keyed terminal event"] Grant --> Await["await handle
ProcessWorkDriver / ProcessAwaiter"] Grant --> Cancel["cancel handle
process.cancel_requested"] Terminal --> Await

A process is self-contained: the record carries its ProcessOriginator (host or session) as pure provenance, a captured execution-environment reference, and an optional 0..1 wake target. Session relationships are explicit, optional edges that never imply cleanup. Deleting a session revokes that session's handle grants and pending wake deliveries, never cancels a process, and the ProcessSessionDeleteReport lists the non-terminal processes left with no remaining grants (orphaned_process_ids); their lifecycle is host policy. See docs/adr/0011-self-contained-processes.md for the full model.

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. Host API, tool, and Lashlang cancel requests route through ProcessCancelAbility with an explicit ProcessCancelSource and reason; cancel-all lists live visible handles and cancels each through that same ability path. Internal cleanup such as cancel_unreferenced remains a registry operation because it is not a public cancel request.

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 whose holder is provably dead is terminalized Abandoned rather than re-run (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: a started OwnerBound holder that is silent but not provably dead is left non-terminal until death evidence or a reconciled Abandon Request. Abandoned is a fourth terminal state, peer to Completed | Failed | Cancelled, carrying the writer (owner drain, sweep, or reconciled request) 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.

Tier Consistency: No Mode Flag

Durability is a property of each execution path, derived from what the host wired, not a mode flag the runtime is told to honor. Store traits and effect-host traits expose a defaulted durability_tier() returning DurabilityTier::Inline; durable implementations override it to Durable. The runtime derives the tier from the wiring and validates consistency at the boundary where the effect-host tier is known.

The rule is simple: a durable effect host running against any ephemeral store facet fails loudly, and it fails where the tier is first observable rather than silently producing a non-durable run. Core's DurabilityTier lives in lash-core; the Lashlang runtime maps its artifact-store tier into the same facade-level coherence checks. The defaulted hook lands on EffectHost / RuntimeEffectController, RuntimePersistence / SessionStoreFactory, LashlangArtifactStore, and ProcessRegistry; AttachmentStore::persistence() is reused as that trait's tier signal (Ephemeral → Inline, Durable → Durable). Durable overrides ship on RestateEffectHost / RestateRuntimeEffectController, the SQLite and Postgres stores / factories / registries, and the local-file and S3 attachment stores; every other implementation inherits Inline untouched, so inline and in-memory hosts work unchanged with no new ceremony.

BoundaryWhereChecks
Turn scope startturn_loop.rs (resume / stream turn-scoped)When the turn's effect host is Durable, every wired store facet must be Durable, else DurableStoreRequired { facet }.
Worker process runDurableProcessWorker::ensure_durable_store_facetsThe same check at process-run and runtime rebuild: a durable worker effect host requires durable attachment, artifact, session, and process-registry stores.
Facade buildlash/src/core.rs::ensure_store_peer_coherenceStore peer-coherence only: the per-invocation durable controller is not visible at build, so build checks stores against each other and never the controller.

The per-invocation durable controller is deliberately invisible at facade build(): a durable controller is constructed per-invocation from a host ctx, so the build-time controller is inline by construction. Build therefore validates peer-coherence among the stores it can see (a durable session store factory requires a durable attachment store and a durable artifact store, and a durable process registry requires a durable session store factory and a durable trigger store), while the effect-host-vs-store check happens later at the turn-scope and worker boundaries where the controller's tier is finally known. The rejected emitter-supplied durability stopgap was deliberately not added: the worker owns execution durability, not whoever emitted the event.

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, native tool calls, durable tool steps and waits, RLM exec, plugin checkpoints, execution-surface sync, retry sleeps, direct completions, direct LLM completions, and controller-mediated process start/list/transfer/delete-session/await/cancel/signal. Turn effects include EffectId and replay key; direct effects include typed causal parents when present and stable direct replay keys.

Outside the seam

Fire-and-forget Emit and Log effects, and side effects a tool implementation performs outside ToolResult, ToolDurableEffects, or a controller-mediated process. Workflows that need those durable should fold them into their own activities.

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.
scoped_transfer_and_cleanup_use_process_effect_controller_metadataProcess handle transfer and unreferenced-handle cleanup route through ProcessOpScope, carrying the active process effect metadata and controller.
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.
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.
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 handle grant, 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_grants_wakes_and_cancelA Restate process start runs through RestateCoreProcessRunner against a SQLite registry/session-store pair, then reopens the registry to list grants, 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.
durable_controller_rejects_ephemeral_{attachment,artifact,session,process_registry}_store_before_turn_runsAt the turn-scope boundary, a durable controller paired with any ephemeral store facet fails with DurableStoreRequired { facet } before the turn runs; durable_controller_with_all_durable_stores_runs_turn confirms the all-durable case proceeds.
durable_worker_rejects_ephemeral_{attachment,artifact,session,process_registry}_storeAt the worker process-run boundary, a durable worker controller paired with any ephemeral store facet fails the store-facet check; durable_worker_with_all_durable_stores_passes_store_facet_check and inline_worker_passes_store_facet_check_with_ephemeral_stores pin the passing cases.
durable_session_store_rejects_ephemeral_{attachment,artifact}_store_at_build / durable_process_registry_rejects_missing_durable_store_factory_at_buildAt facade build(), store peer-coherence is validated without ever inspecting the controller; all_durable_stores_build_successfully confirms a fully durable wiring builds.
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 ·