paperclipai/paperclip · info

[paperclip] Workspace restore preserved local working tree…

Error message

[paperclip] Workspace restore preserved local working tree changes after clean sandbox restore.

What it means

resetLocalGitIndexToHead runs `git reset --quiet HEAD -- .` to unstage everything after a sandbox restore, asserts no staged changes remain (throws if staged diff is non-empty), then — only when checkWorkingTreeClean is set — runs `git diff --name-status HEAD`. Non-empty output means the restore left unstaged working-tree modifications, which the helper intentionally does NOT discard; this console.warn says those local changes were preserved through the clean-restore flow.

Solutions

  1. Inspect what survived: run `git status --porcelain` and `git diff` in the workspace directory.
  2. If you need a pristine tree, commit or stash the local changes, or explicitly clean them (git checkout -- . / git clean).
  3. Treat the message as expected behavior when local edits existed before the restore — data was deliberately preserved.
Defensive patterns

Strategy: validation

Validate before calling

import { execSync } from "node:child_process";
const dirty = execSync("git status --porcelain", { cwd: workspaceDir, encoding: "utf8" });
if (dirty.trim().length > 0) {
  // decide before restore: commit, stash, or accept that restore preserves these edits
}

Type guard

const isCleanStatus = (porcelain: string): boolean => porcelain.trim().length === 0;

Prevention

When it happens

Trigger: A workspace that had local uncommitted edits (hotfixes, generated files) goes through a clean sandbox restore; the index resets to HEAD but the modified files remain on disk, so `git diff HEAD` still reports them.

Common situations: Agent run restored from a sandbox while a developer had local edits in the same checkout; generated artifacts differing from HEAD; end-of-line or permission-only diffs.

Related errors


AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-08-18). Data as JSON: /api/errors/584db90a459541f0. Report an issue: GitHub.

Appendix: source

Thrown at packages/adapter-utils/src/git-workspace-sync.ts:702

  forceFullBundle?: boolean;
}): string {
  const remoteDir = shellQuote(input.remoteDir);
  const bundlePath = shellQuote(input.bundlePath);
  const exportRef = shellQuote(input.exportRef);
  const baseSha = shellQuote(input.baseSha);
  const statusPath = input.statusPath ? shellQuote(input.statusPath) : null;
  const cleanupParts = [
    `rm -f ${bundlePath}`,
    ...(statusPath ? [`rm -f ${statusPath}`] : []),
    `git -C ${remoteDir} update-ref -d ${exportRef} >/dev/null 2>&1 || true`,
  ];
  return [
    "set -e",
    input.cleanupBundle ? `cleanup() { ${cleanupParts.join("; ")}; }` : "",
    input.cleanupBundle ? "trap cleanup EXIT" : "",
    `mkdir -p ${shellQuote(path.posix.dirname(input.bundlePath))}`,
    `rm -f ${bundlePath}`,
    // Choose the bundle boundary. A thin bundle `HEAD --not <baseSha>` records
    // baseSha as a prerequisite the importer (host) must already hold. That
    // assumption breaks in two real cases, and then `git fetch` on the host
    // hard-fails with "did not send all necessary objects" and the run's work
    // is lost:
    //   1. The sandbox HEAD has diverged from baseSha (e.g. a local-only branch
    //      that forked from an older commit) — the host may still hold baseSha,
    //      but a repo whose history is inconsistent cannot satisfy the walk.
    //   2. The host workspace no longer holds baseSha at import time (a shared
    //      workspace that was reset/re-realized between export and import).
    // Bundle relative to the merge-base of baseSha and HEAD instead: that
    // merge-base is an ancestor of baseSha, so any host that holds baseSha (or
    // an ancestor of it) can satisfy the prerequisite, while the bundle stays a
    // delta. When baseSha is absent from the sandbox — or no merge-base exists,
    // or the caller forces it after a delta import failed on a missing
    // prerequisite — fall back to a full, self-contained bundle with no
    // prerequisites.
    ...(input.forceFullBundle
      ? [`bundle_base=""`]

View on GitHub (pinned to 3f1d897a7c)