Error Codes Reference
Errors produced by the UndoLog runtime across all crates. The UndoLogError enum is defined in crates/undolog-types/src/errors.rs.
Rust type: undolog_types::errors::UndoLogError
Result alias: UndoLogResult<T> = Result<T, UndoLogError>
Error Enum
ToolNotRegistered
| Property | Value |
|---|---|
| Variant | UndoLogError::ToolNotRegistered { tool_name, org_id } |
| Message | Tool '{tool_name}' is not registered for org '{org_id}' |
When raised: The engine receives a tool call for a tool name that does not exist in undolog_tool_registry for the given organisation.
Example scenario: An agent calls delete_invoice but the tool was never registered via the SDK annotation.
How to handle: Register the tool using the SDK @undolog_tool decorator. Verify the tool name matches the registry entry. Check organisation ID is correct.
DuplicateSignature
| Property | Value |
|---|---|
| Variant | UndoLogError::DuplicateSignature { signature } |
| Message | Call signature conflict: effect with signature '{signature}' already exists |
When raised: An INSERT on undolog_effect_log conflicts with an existing call_signature and the advisory lock already prevented the race. This error is returned when the engine cannot resolve the conflict automatically.
Example scenario: Two concurrent proxy instances attempt to insert the same tool call simultaneously and the advisory lock + UNIQUE constraint interact unexpectedly.
How to handle: This is a safety-net error. Under normal operation the ON CONFLICT DO NOTHING + Replay path handles duplicates. If raised, retry the call or check for session state corruption.
InvalidStateTransition
| Property | Value |
|---|---|
| Variant | UndoLogError::InvalidStateTransition { effect_id, current_state, target_state } |
| Message | Effect '{effect_id}' is in state '{current_state}', cannot transition to '{target_state}' |
When raised: An attempt is made to transition an effect to a state that is not reachable from its current state per the state machine rules.
Example scenario: Calling Commit on an effect that is in Pending state instead of Executing.
How to handle: Verify the effect's current state via the effect log. Ensure the correct RPC sequence is followed: Intercept → execute → Commit/Fail. Do not call Commit twice.
NotExecuting
| Property | Value |
|---|---|
| Variant | UndoLogError::NotExecuting { effect_id } |
| Message | Cannot commit effect '{effect_id}': not in 'executing' state |
When raised: Commit is called on an effect that is not in the Executing state.
Example scenario: The proxy calls Commit after a replay (effect is already Committed or Replayed).
How to handle: Only call Commit after receiving an Execute outcome from Intercept. Do not call Commit for Replay or AwaitingApproval outcomes.
CompensationFailed
| Property | Value |
|---|---|
| Variant | UndoLogError::CompensationFailed { fn_name, retries, reason } |
| Message | Compensation '{fn_name}' failed after {retries} retries: {reason} |
When raised: The compensation function for a Compensable tool exhausted all retry attempts without succeeding.
Example scenario: undo_send_email compensation called after a session failure, but the upstream email API is down and all 3 retries failed.
How to handle: Manual intervention is required. Inspect the compensation registry and the upstream service. Resolve the underlying issue, then retry compensation manually or mark the session as compensated.
EmptyUndoStack
| Property | Value |
|---|---|
| Variant | UndoLogError::EmptyUndoStack { session_id } |
| Message | Undo stack for session '{session_id}' is empty |
When raised: The saga orchestrator attempts to pop from the undo stack, but there are no pending compensation entries for the session.
Example scenario: A session fails but no Compensable tools were executed (only Safe and Irreversible tools).
How to handle: This is an informational error. No compensation is needed if the undo stack is empty.
CompensationRegisteredTooLate
| Property | Value |
|---|---|
| Variant | UndoLogError::CompensationRegisteredTooLate { effect_id } |
| Message | Compensation registered after action executed for effect '{effect_id}', invariant violated |
When raised: The saga orchestrator detects that a compensation entry was registered after the tool execution timestamp, violating the core safety invariant.
Example scenario: A bug in the orchestrator causes the undo stack push to happen after the tool call instead of before.
How to handle: This indicates a bug in the orchestrator logic. The invariant registered_at < executed_at must always hold. Fix the ordering in the orchestrator code.
ApprovalAlreadyResolved
| Property | Value |
|---|---|
| Variant | UndoLogError::ApprovalAlreadyResolved { approval_id } |
| Message | Approval request '{approval_id}' has already been resolved |
When raised: An Approve or Reject RPC is called for an approval request that is already in a terminal state.
Example scenario: Two dashboard users both click "Approve" on the same approval request. The second request returns this error.
How to handle: The approval has already been processed. The caller should refresh the approval list and check the current state.
ApprovalTimedOut
| Property | Value |
|---|---|
| Variant | UndoLogError::ApprovalTimedOut { approval_id } |
| Message | Approval request '{approval_id}' timed out |
When raised: The approval window expired before a human decision was made. The approval record transitions to TimedOut or AutoApproved depending on org policy.
Example scenario: An approval request was created with timeout_at of 1 hour. No decision was made within that window.
How to handle: If auto_approve_on_timeout is true, the tool auto-executes. If false, the agent must be re-invoked with a new session.
AdvisoryLockTimeout
| Property | Value |
|---|---|
| Variant | UndoLogError::AdvisoryLockTimeout { signature, attempts } |
| Message | Could not acquire advisory lock for signature '{signature}' after {attempts} attempts |
When raised: The engine could not acquire a PostgreSQL advisory lock for a call signature after UNDOLOG_LOCK_MAX_ATTEMPTS attempts.
Example scenario: High concurrency on the same tool call signature, causing repeated lock collisions.
How to handle: Retry the operation. If persistent, increase UNDOLOG_LOCK_MAX_ATTEMPTS or UNDOLOG_LOCK_RETRY_MS. The lock is only held for the duration of the INSERT transaction (typically < 100ms).
Database
| Property | Value |
|---|---|
| Variant | UndoLogError::Database(sqlx::Error) |
| Message | Database error: {0} |
When raised: Any SQL-level error from PostgreSQL: connection failure, constraint violation, deadlock, serialisation failure, partition missing, etc.
Example scenario: PostgreSQL connection pool exhausted during a traffic spike. A query violates a CHECK constraint.
How to handle: Check database connectivity and pool settings. Inspect the inner sqlx::Error for the specific SQL error code. Most database errors are transient and can be retried.
Serialization
| Property | Value |
|---|---|
| Variant | UndoLogError::Serialization(serde_json::Error) |
| Message | Serialization error: {0} |
When raised: JSON serialisation or deserialisation fails for internal data structures (tool calls, results, compensation args, etc.).
Example scenario: A tool returns non-UTF-8 output that cannot be serialised. Internal enum variant does not match the JSON representation.
How to handle: Validate tool inputs and outputs are valid JSON. Check for version mismatches between the proxy and engine (new enum variants that the other side does not know about).
Internal
| Property | Value |
|---|---|
| Variant | UndoLogError::Internal(String) |
| Message | Internal error: {0} |
When raised: Any unexpected runtime error that does not fit into the above categories. Catch-all for programming errors.
Example scenario: A failed tokio::spawn join handle. An unexpected None where a value was expected.
How to handle: This is a bug. Report the error message along with the session and effect IDs for debugging.
Error Handling Quick Reference
| Error | Retryable | Manual intervention | Check |
|---|---|---|---|
ToolNotRegistered |
No | Yes | Tool registry |
DuplicateSignature |
Yes | No | Retry call |
InvalidStateTransition |
No | Yes | Effect state |
NotExecuting |
No | Yes | Effect state |
CompensationFailed |
No | Yes | Upstream service |
EmptyUndoStack |
No | No | Informational |
CompensationRegisteredTooLate |
No | Yes | Orchestrator code |
ApprovalAlreadyResolved |
No | No | Refetch approvals |
ApprovalTimedOut |
No | Depends | Timeout policy |
AdvisoryLockTimeout |
Yes | No | Increase lock settings |
Database |
Yes | Depends | DB connectivity |
Serialization |
No | Yes | Input/output format |
Internal |
No | Yes | Report as bug |