How the Pi Web RPC Manager Handles Concurrent Session Start Requests with Locks

The RPC manager in agegr/pi-web uses a global lock map stored on globalThis to guarantee that only one AgentSession is instantiated per logical session ID, even when multiple HTTP requests attempt to start the same session simultaneously.

The agegr/pi-web repository implements a sophisticated concurrency control mechanism within its RPC layer to prevent race conditions during session initialization. When multiple API routes or clients attempt to initiate the same logical session concurrently, the system risks creating duplicate session files or conflicting state. The RPC manager handles concurrent session start requests with locks by maintaining a singleton promise map that serializes the session creation pipeline across the entire application lifecycle.

The Global Lock Architecture

The concurrency strategy centers on a module-level Map called __piStartLocks that resides on globalThis. This design ensures the lock state persists across Next.js hot reloads, preventing duplicate session creation even when the development server restarts. The map stores promises representing in-flight session creation operations, keyed by sessionId.

When startRpcSession() is invoked in lib/rpc-manager.ts, it executes a four-phase validation and locking sequence:

Step 1: Registry Check for Existing Sessions

First, the function checks the session registry to determine if a live wrapper already exists for the requested sessionId. If registry.get(sessionId) returns a valid instance (lines 48-49), the function returns the existing session immediately, bypassing the locking mechanism entirely.

Step 2: Lock Inspection for In-Flight Creation

If no active session exists, the manager inspects the global lock map. When locks.get(sessionId) returns a promise (lines 51-52), it indicates another request is currently initializing the session. The function returns this stored promise directly, allowing both callers to share the same resolution result and avoid redundant work.

Step 3: Atomic Lock Creation and Promise Storage

When no lock exists, the manager creates a new promise (starting) that encapsulates the entire initialization flow: theme initialization, SessionManager creation and opening, service construction, and wrapper instantiation. Before execution begins, this promise is stored in the lock map via locks.set(sessionId, starting) (lines 113-114), ensuring subsequent concurrent requests detect the in-flight operation.

Step 4: Lock Release and Cleanup

The stored promise includes a finally block that removes the lock entry via locks.delete(sessionId) (lines 111-112). This cleanup executes regardless of whether the session creation succeeds or fails, ensuring the lock map remains clean and subsequent requests can create a fresh lock if needed.

Surviving Next.js Hot Reloads with globalThis

Unlike module-level variables that reset during development reloads, the __piStartLocks map is explicitly attached to globalThis. This architectural decision prevents the "ghost session" problem where a reload might otherwise allow a new request to create a duplicate session while an older initialization was still pending. The global scope ensures idempotent session creation across the entire process lifetime.

Practical Implementation Example

The following TypeScript example demonstrates how simultaneous API route handlers safely share the same session instance:

// Example: Simultaneous calls from two API routes
import { startRpcSession } from "@/lib/rpc-manager";

async function handlerA() {
  // Both handlers request the same logical sessionId
  const { session } = await startRpcSession("my-session", "", "/home/user/project");
  // …use session
}

async function handlerB() {
  const { session } = await startRpcSession("my-session", "", "/home/user/project");
  // …use session
}

// When handlerA and handlerB run concurrently, only one
// AgentSessionWrapper is instantiated. Both receive the same
// `session` instance because the second call sees the lock
// created by the first call.

For debugging or monitoring purposes, you can inspect the current lock state:

// Manual lock inspection (for debugging)
import { getLocks } from "@/lib/rpc-manager";

function isSessionStarting(id: string): boolean {
  return !!getLocks().get(id);
}

// Returns true while the async startRpcSession promise is pending.

Summary

  • Registry-first validation: startRpcSession() checks existing sessions in registry before attempting to acquire locks.
  • Promise-based locking: The __piStartLocks map stores promises rather than boolean flags, enabling direct result sharing between concurrent callers.
  • Global scope persistence: Attaching the lock map to globalThis prevents duplicate sessions across Next.js hot reloads.
  • Automatic cleanup: The finally block in lib/rpc-manager.ts (lines 111-112) guarantees lock removal regardless of initialization success or failure.
  • Idempotent guarantees: The pattern ensures that calling startRpcSession() multiple times with the same ID returns the identical session instance.

Frequently Asked Questions

What happens if two API routes call startRpcSession simultaneously?

Both requests receive the same AgentSession instance. The first call creates a lock promise stored in __piStartLocks, while the second call detects this lock at lines 51-52 and returns the existing promise. Both callers await the same resolution, ensuring they share the identical session wrapper.

How does the lock map survive Next.js development reloads?

The lock map is stored on globalThis rather than as a module-level variable. Since globalThis persists across module reloads in Node.js, the __piStartLocks map retains its state during hot reloads, preventing duplicate session creation that could otherwise occur if the map were reinitialized on every import.

Where is the session registry stored compared to the lock map?

The session registry is a separate module-level Map that stores active AgentSessionWrapper instances, while the lock map (__piStartLocks) stores transient promises representing sessions currently being created. The registry check occurs first (lines 48-49), followed by the lock check (lines 51-52), ensuring completed sessions short-circuit before the locking logic executes.

When is the lock entry deleted from __piStartLocks?

The lock entry is removed in the finally block of the initialization promise at lines 111-112 in lib/rpc-manager.ts. This executes after the session completes initialization (successfully or with an error), ensuring the lock is always released and the session ID can be reused for future creation attempts if needed.

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 →