paperclipai/paperclip · error · Error

Workspace database repair command timed out during

Error message

Workspace database repair command timed out during ${repairPhase}.

What it means

Thrown when the repair reseed child process exceeds the hard 20-minute budget. The route spawns node <cli> worktree reseed ... and arms a timer (server/src/routes/execution-workspaces.ts:715-720) that SIGTERMs the child at 20 minutes and SIGKILLs 5 seconds later; a non-zero exit with repairCommandTimedOut=true produces this message, naming the outer repair phase (target_backup or full_reseed) the flow had reached. The worktree itself is preserved (worktreePreserved, databaseOnly) and the failed phase is reported on the workspace operation.

Solutions

  1. Check the workspace operation record and captured child stdout/stderr to see how far the reseed got before the kill.
  2. Eliminate I/O contention: stop concurrent repairs/seeds against the same base, repair from faster local disk.
  3. Shrink the seed scope if the CLI supports it (seed mode filters), or prune/archieve old data in the base workspace database.
  4. Retry the repair — the target is backed up first (--backup-target) so a timed-out attempt is safe to re-run.
  5. If large databases are the norm, raise the fixed 20-minute budget in server/src/routes/execution-workspaces.ts:715 as a code change.
Defensive patterns

Strategy: retry

Validate before calling

// Estimate before repair: check base .paperclip database size on disk
const sizeBytes = await du(path.join(baseCwd, ".paperclip"));
if (sizeBytes > LARGE_SEED_THRESHOLD) { /* schedule during low load, or prune base data first */ }

Try / catch

try {
  await repair(id);
} catch (err) {
  if (/timed out during/.test(String(err?.body?.error ?? err?.message ?? ""))) {
    // reseed target is backed up and worktree preserved — safe to retry after reducing I/O load
    await waitForIdleIo(); await repair(id);
  } else throw err;
}

Prevention

When it happens

Trigger: POST repair where the full reseed of the worktree database takes longer than 20 minutes: very large base database, slow disk/network-attached storage, lock contention on the base .paperclip database, or the child hanging on a prompt because --yes did not cover an interactive path. The interval poll (manifestPoll) determines whether the phase is reported as full_reseed vs target_backup.

Common situations: Base workspace with a multi-GB PGlite/pg data directory copied over NFS; a seeded company with huge issue/comment history; concurrent repairs or an active seed against the same base starving I/O; a CLI version that stalls waiting for a lock on the source database.

Understand the failure class

Related errors


AI-assisted analysis of paperclipai/paperclip@a7e689b3c3 (2026-08-21). Data as JSON: /api/errors/3a3f2eb8383a49ca. Report an issue: GitHub.

Appendix: source

Thrown at server/src/routes/execution-workspaces.ts:775

              let seedFailurePhase: string | null = null;
              try {
                const failedManifest = JSON.parse(readFileSync(manifestPath, "utf8")) as {
                  phase?: unknown;
                  state?: unknown;
                };
                seedFailurePhase = failedManifest.state === "failed" && typeof failedManifest.phase === "string"
                  ? failedManifest.phase
                  : null;
              } catch {
                // The outer repair phase remains exact when no seed phase was persisted.
              }
              await reportProgress({
                metadata: { seedFailurePhase },
                system: seedFailurePhase ? `Workspace seed failed during ${seedFailurePhase}.\n` : null,
              });
              throw new Error(
                repairCommandTimedOut
                  ? `Workspace database repair command timed out during ${repairPhase}.`
                  : `Workspace database repair command failed during ${repairPhase}.`,
              );
            }
            if (!reseedObserved) {
              await reportRepairPhase("target_backup", "succeeded");
              await reportRepairPhase("full_reseed", "started");
            }

            const manifest = JSON.parse(readFileSync(manifestPath, "utf8")) as Record<string, unknown>;
            if (!isVerifiedWorktreeSeedManifest(manifest)) {
              throw new Error("Workspace reseed returned without a verified terminal manifest.");
            }
            const expectedInstanceId = resolveManagedWorkspaceInstanceId(workspaceCwd);
            if (manifest.targetInstanceId !== expectedInstanceId) {
              throw repairPreconditionError(
                422,
                "seed_manifest_instance_mismatch",
                "Verified seed manifest belongs to a different workspace instance.",

View on GitHub (pinned to a7e689b3c3)