# How to Handle Stale Epoch Errors from Concurrent Mutations in pi-computer-use

> Learn how to handle stale epoch errors from concurrent mutations in pi-computer-use. Implement the catch-refresh-retry pattern to ensure state consistency.

- Repository: [injaneity/pi-computer-use](https://github.com/injaneity/pi-computer-use)
- Tags: how-to-guide
- Published: 2026-07-16

---

**pi-computer-use implements optimistic concurrency control through an epoch-based resource scheduler that throws `StaleResourceStateError` when concurrent mutations conflict, requiring a catch-refresh-retry pattern to maintain state consistency.**

pi-computer-use manages mutable resources such as desktop snapshots and browser pages through a centralized **resource scheduler** that tracks state using incremental *epoch* numbers. When two asynchronous actions attempt to modify the same resource simultaneously, the scheduler detects conflicts by comparing the expected epoch against the actual current value. Understanding how to properly handle these stale epoch errors ensures your automation scripts recover gracefully from race conditions without requiring heavyweight locking mechanisms.

## Understanding the Epoch-Based Concurrency Model

The `ResourceScheduler` implemented in [`src/runtime.ts`](https://github.com/injaneity/pi-computer-use/blob/main/src/runtime.ts) assigns every mutable resource an incremental epoch number that advances with each successful mutation. Every read or write operation first validates that the provided epoch matches the resource’s current state.

According to the pi-computer-use source code in [`src/runtime.ts`](https://github.com/injaneity/pi-computer-use/blob/main/src/runtime.ts), the scheduler performs this check before committing any write:

```typescript
if (record.epoch !== expectedEpoch) throw new StaleResourceStateError(...);

```

This **optimistic concurrency** approach avoids the performance overhead of locks while guaranteeing that mutations apply only to the most recent state.

## Recognizing StaleResourceStateError

When a mutation fails because another operation advanced the epoch first, the scheduler throws **`StaleResourceStateError`**. This error is defined in [`src/runtime.ts`](https://github.com/injaneity/pi-computer-use/blob/main/src/runtime.ts) and is deliberately designed as a recoverable concurrency conflict rather than a fatal exception.

Key indicators that you have encountered this error:
- The error instance checks `true` for `err instanceof StaleResourceStateError`
- It occurs during `scheduler.write()` or `scheduler.readAt()` calls
- It indicates the `baseEpoch` you provided no longer matches the resource’s current epoch

## Implementing the Catch-and-Retry Pattern

The standard recovery pattern involves three steps implemented around your mutation logic in [`src/bridge.ts`](https://github.com/injaneity/pi-computer-use/blob/main/src/bridge.ts) and similar action files.

### Catch the Error at the Mutation Site

Wrap your `scheduler.write` calls in a try-catch block specifically targeting `StaleResourceStateError`. As shown in [`src/bridge.ts`](https://github.com/injaneity/pi-computer-use/blob/main/src/bridge.ts) around the resource mutation logic, you should isolate the epoch check within a dedicated error handler:

```typescript
try {
  const { value: newState, epoch: nextEpoch } =
    await scheduler.write(resourceKey, baseEpoch, async (next) => {
      // Mutation logic here
      return { ...state, epoch: next };
    });
} catch (err) {
  if (err instanceof StaleResourceStateError) {
    // Handle retry logic here
  }
  throw err; // Re-throw unexpected errors
}

```

### Refresh the Current Epoch

Upon catching the error, fetch the latest epoch from the scheduler to update your local view. You can use `scheduler.read` to retrieve the current value without modifying state, or call `resourceScheduler.restoreEpoch()` as implemented in [`src/bridge.ts`](https://github.com/injaneity/pi-computer-use/blob/main/src/bridge.ts) (lines 2252-2257):

```typescript
const refreshed = await scheduler.read(resourceKey, async (curEpoch) => curEpoch);
state.epoch = refreshed; // Update local state with latest epoch

```

### Retry with the Fresh Epoch

Once you have synchronized your epoch with the scheduler, recursively or iteratively retry the original mutation. Because the scheduler increments the epoch atomically during successful writes, your retry will succeed unless another concurrent mutation intervenes, in which case the cycle repeats.

## Complete Code Example

Here is a production-ready implementation showing the full optimistic concurrency pattern:

```typescript
async function updateDesktop(
  state: DesktopState,
  resourceKey: string,
  scheduler: ResourceScheduler,
  savedStates: SavedStates,
) {
  try {
    // Capture the expected epoch
    const baseEpoch = state.epoch ?? scheduler.epoch(resourceKey);

    // Attempt the write with epoch verification
    const { value: newState, epoch: nextEpoch } =
      await scheduler.write(resourceKey, baseEpoch, async (next) => {
        return { ...state, epoch: next };
      });

    // Persist successful mutation
    savedStates.saveDesktop(newState, resourceKey, nextEpoch);
    state.epoch = nextEpoch;
  } catch (err) {
    // Handle stale epoch specifically
    if (err instanceof StaleResourceStateError) {
      const refreshed = await scheduler.read(resourceKey, async (e) => e);
      state.epoch = refreshed;
      return updateDesktop(state, resourceKey, scheduler, savedStates); // Retry
    }
    throw err;
  }
}

```

For simple epoch retrieval without mutation, use this helper:

```typescript
async function getCurrentEpoch(
  resourceKey: string,
  scheduler: ResourceScheduler,
): Promise<number> {
  const { epoch } = await scheduler.read(resourceKey, async (e) => e);
  return epoch;
}

```

## Key Files and Architecture

Understanding where these components live helps debug concurrency issues:

- **[`src/runtime.ts`](https://github.com/injaneity/pi-computer-use/blob/main/src/runtime.ts)** – Contains the `ResourceScheduler` class, epoch bookkeeping logic, and the `StaleResourceStateError` definition
- **[`src/bridge.ts`](https://github.com/injaneity/pi-computer-use/blob/main/src/bridge.ts)** – Houses high-level mutation actions that catch and retry on stale epochs (see lines 425 and 2252-2257 for concrete examples)
- **[`src/state.ts`](https://github.com/injaneity/pi-computer-use/blob/main/src/state.ts)** – Defines the `OperationState` interface that carries the optional `epoch` property used throughout the system
- **[`src/view.ts`](https://github.com/injaneity/pi-computer-use/blob/main/src/view.ts)**, **[`src/output.ts`](https://github.com/injaneity/pi-computer-use/blob/main/src/output.ts)**, **[`src/actions.ts`](https://github.com/injaneity/pi-computer-use/blob/main/src/actions.ts)** – Consume the bridge’s mutation helpers and indirectly benefit from the same error handling

## Summary

- **pi-computer-use** tracks resource state using incremental **epoch numbers** that advance with every successful mutation in [`src/runtime.ts`](https://github.com/injaneity/pi-computer-use/blob/main/src/runtime.ts)
- **StaleResourceStateError** signals that your operation used an outdated epoch due to concurrent modification
- The recovery pattern requires **catching the error**, **refreshing the epoch** via `scheduler.read()`, and **retrying the mutation** with the updated value
- This optimistic concurrency approach eliminates locking overhead while ensuring mutations apply only to current state

## Frequently Asked Questions

### What causes a StaleResourceStateError in pi-computer-use?

A `StaleResourceStateError` occurs when two asynchronous operations attempt to modify the same mutable resource (like a desktop snapshot) simultaneously. The first operation succeeds and increments the resource's epoch, causing the second operation—which was based on the old epoch—to fail when [`src/runtime.ts`](https://github.com/injaneity/pi-computer-use/blob/main/src/runtime.ts) validates the expected epoch against the actual current value.

### How do I distinguish StaleResourceStateError from other exceptions?

Check the error type using the `instanceof` operator: `if (err instanceof StaleResourceStateError)`. This class is specifically exported from [`src/runtime.ts`](https://github.com/injaneity/pi-computer-use/blob/main/src/runtime.ts) and used exclusively for epoch mismatches, unlike network errors or validation errors that may occur elsewhere in the system.

### Can I prevent stale epoch errors by using locks instead of retries?

The pi-computer-use architecture explicitly avoids locks in favor of optimistic concurrency to maintain performance. While you could theoretically implement external locking, the `ResourceScheduler` does not provide native lock mechanisms. The intended and most efficient approach is to implement the catch-refresh-retry pattern shown in [`src/bridge.ts`](https://github.com/injaneity/pi-computer-use/blob/main/src/bridge.ts), which handles contention gracefully with minimal overhead.

### Where should I store the epoch between operations?

Store the epoch in your local state object that implements the `OperationState` interface defined in [`src/state.ts`](https://github.com/injaneity/pi-computer-use/blob/main/src/state.ts). This object should carry an optional `epoch` property that you update after every successful read or write operation. Passing this cached epoch to subsequent `scheduler.write()` calls enables the conflict detection mechanism.