# Preventing Double Work with Paperclip AI Task Checkout and Locking: A Complete Guide

> Prevent double work with Paperclip AI's task checkout and locking system. Ensure only one agent owns an issue at a time, with automatic fallback for seamless collaboration.

- Repository: [Paperclip/paperclip](https://github.com/paperclipai/paperclip)
- Tags: how-to-guide
- Published: 2026-08-12

---

**Paperclip AI prevents duplicate work by persisting a exclusive checkout lock in the database, allowing only one run (agent or board) to own an issue at a time while automatically falling back to board-level coordination when conflicts occur.**

The Paperclip AI repository (`paperclipai/paperclip`) implements a robust concurrency control system that eliminates race conditions when multiple execution contexts attempt to work the same issue. By storing lock state directly in the `issues` table and enforcing ownership through database-level validation, the system guarantees exactly-once execution semantics across process restarts and distributed deployments.

## How Database-Level Locking Prevents Duplicate Work

At the core of Paperclip AI’s concurrency model are two columns in the `issues` table: `checkoutRunId` and `executionRunId`. These nullable string fields track which run currently owns the checkout lock and which run is actively executing the issue, respectively.

The lock validation logic resides in **[`server/src/services/issues.ts`](https://github.com/paperclipai/paperclip/blob/main/server/src/services/issues.ts)** within the `sameRunLock` helper function:

```typescript
function sameRunLock(checkoutRunId: string | null, actorRunId: string | null) {
  if (actorRunId) return checkoutRunId === actorRunId;
  return checkoutRunId == null;
}

```

This function implements the ownership semantics: an actor can acquire the lock only if the stored `checkoutRunId` matches its own run ID, or if no lock exists (`null`). When an agent or board attempts to checkout an issue via `POST /api/issues/:id/checkout`, the server validates the request against this rule before writing the lock.

## Implementing the Checkout Endpoint in [`server/src/services/issues.ts`](https://github.com/paperclipai/paperclip/blob/main/server/src/services/issues.ts)

The checkout endpoint handler demonstrates the complete lock acquisition flow. When a request arrives, the server extracts the `actorRunId` from the request context and queries the current issue state:

```typescript
// server/src/services/issues.ts – checkout endpoint handler
export const checkout = async (req, res) => {
  const { id } = req.params;
  const actorRunId = req.actor?.runId ?? null;

  const issue = await db.selectIssue(id);
  if (!sameRunLock(issue.checkoutRunId, actorRunId)) {
    return res.status(409).json({ error: 'Issue already checked out' });
  }

  await db.update(issue.id, { checkoutRunId: actorRunId });
  await activity.log('issue.checkout_lock_adopted', {
    issueId: issue.id,
    checkoutRunId: actorRunId,
  });
  return res.json(issue);
};

```

If validation fails—meaning another run already holds the lock—the server immediately returns **HTTP 409 Conflict**, preventing the double-work scenario. On success, the system persists the new `checkoutRunId` and emits an `issue.checkout_lock_adopted` activity event for observability.

## Handling Lock Conflicts with Agent-to-Board Fallback

Production deployments must handle transient lock failures gracefully. The end-to-end test suite in **[`tests/e2e/signoff-policy.spec.ts`](https://github.com/paperclipai/paperclip/blob/main/tests/e2e/signoff-policy.spec.ts)** demonstrates the recommended client-side pattern: attempt an agent checkout first, then fall back to a board checkout if the lock is unavailable.

```typescript
const checkoutRes = await agent.request.post(
  `${BASE_URL}/api/issues/${issueId}/checkout`,
  { agentId }
);

if (!checkoutRes.ok() && checkoutRes.status() === 409) {
  // Lock held by another agent; escalate to board-level coordination
  const boardCheckout = await board.post(
    `${BASE_URL}/api/issues/${issueId}/checkout`,
    {}
  );
  // Board checkout succeeds if the agent lock has expired or is invalid
}

```

This **agent-first, board-fallback** strategy ensures that automated agents can work independently when possible, while the board UI serves as a coordinator of last resort when exclusive locks are contested.

## Activity Logging and Audit Trails

Every successful lock adoption generates an activity record via **[`server/src/services/plugin-host-services.ts`](https://github.com/paperclipai/paperclip/blob/main/server/src/services/plugin-host-services.ts)**. The `issue.checkout_lock_adopted` event captures the `issueId` and `checkoutRunId`, creating an immutable audit trail of which execution context owned the issue at any point in time. This logging supports debugging race conditions and provides visibility into workload distribution across the agent fleet.

## Automatic Cleanup of Stale Locks

To prevent deadlocks from orphaned locks when processes crash or runs terminate unexpectedly, Paperclip AI includes a recovery service in **[`server/src/services/recovery/service.ts`](https://github.com/paperclipai/paperclip/blob/main/server/src/services/recovery/service.ts)**. This background job periodically scans for `checkoutRunId` and `executionRunId` values referencing non-existent runs:

```typescript
// server/src/services/recovery/service.ts – stale lock sweeper
await db
  .update(issues)
  .set({ checkoutRunId: null, executionRunId: null })
  .where(sql`(${issues.checkoutRunId} IS NOT NULL OR ${issues.executionRunId} IS NOT NULL)`);

```

By clearing these columns when the associated run records are missing, the sweeper guarantees that transient failures never permanently block issue processing.

## Summary

- **Database-backed exclusivity**: The `checkoutRunId` column in [`server/src/services/issues.ts`](https://github.com/paperclipai/paperclip/blob/main/server/src/services/issues.ts) serves as the single source of truth for issue ownership, surviving process restarts and scaling across multiple server replicas.
- **409 Conflict semantics**: Failed checkout attempts return HTTP 409, enabling clients to implement intelligent fallback logic rather than silently duplicating work.
- **Agent/board coordination**: The `sameRunLock` function and activity logging in [`server/src/services/plugin-host-services.ts`](https://github.com/paperclipai/paperclip/blob/main/server/src/services/plugin-host-services.ts) support seamless handoff between automated agents and human-driven board workflows.
- **Automatic recovery**: The periodic cleanup job in [`server/src/services/recovery/service.ts`](https://github.com/paperclipai/paperclip/blob/main/server/src/services/recovery/service.ts) prevents indefinite deadlocks by garbage-collecting orphaned locks from crashed runs.

## Frequently Asked Questions

### What happens if two agents try to checkout the same issue simultaneously?

The first agent to commit its transaction writes its `runId` to the `checkoutRunId` column. The second agent’s request fails the `sameRunLock` validation and receives an HTTP 409 Conflict response, forcing it to retry or fall back to board-level coordination.

### How does Paperclip AI handle locks when a process crashes mid-execution?

Because locks are stored in the database rather than in-memory, they persist across restarts. The recovery service in [`server/src/services/recovery/service.ts`](https://github.com/paperclipai/paperclip/blob/main/server/src/services/recovery/service.ts) periodically queries for locks belonging to non-existent runs and clears them, ensuring crashed agents cannot indefinitely block issue processing.

### Can a board override an agent's lock?

No—the `sameRunLock` function enforces strict ownership. However, the board can acquire the lock once the agent’s run record is cleaned up by the recovery service, or if the agent explicitly releases the lock by setting `checkoutRunId` to `null` upon completion.

### Where is the lock state persisted in the database?

The lock state is stored in the `issues` table as the `checkoutRunId` and `executionRunId` columns, as defined in the schema referenced by [`server/src/services/issues.ts`](https://github.com/paperclipai/paperclip/blob/main/server/src/services/issues.ts) and cleaned by [`server/src/services/recovery/service.ts`](https://github.com/paperclipai/paperclip/blob/main/server/src/services/recovery/service.ts).