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

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 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, the scheduler performs this check before committing any write:

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 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 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 around the resource mutation logic, you should isolate the epoch check within a dedicated error handler:

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 (lines 2252-2257):

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:

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:

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 – Contains the ResourceScheduler class, epoch bookkeeping logic, and the StaleResourceStateError definition
  • 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 – Defines the OperationState interface that carries the optional epoch property used throughout the system
  • src/view.ts, src/output.ts, 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
  • 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 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 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, 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. 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →