Skip to content
Français

Recover work

Inspect retained worktrees and transfers before restoring or cleaning them up.

When a run stops before its changes can be integrated, inspect the work Outpost retained. Use the recovery inventory to locate worktrees, downloaded transfers and backups before restoring or removing anything.

WhatWhereKept when
Worktree.outpost/workspaces/The run failed, integration conflicted, or the worktree is dirty, detached or holds ignored files (node_modules, copies).
Remote transfer.outpost/recovery/Changes from a cloud sandbox could not be applied to your checkout.
Conversation.outpost/conversations/ or the agent’s own storeAfter each turn and on failure. See Conversations.
Workflow progress.outpost/storage/ or your transportAfter each finished task. See Durable runs.

A retained worktree is an ordinary Git worktree on its branch: open it, commit what you keep and merge the branch.

recoveryDetails() returns what Outpost attached to the error: branch, directory, commits, transcript and logReference when available.

import { reportValue } from "./reporter.ts";
import { dispatch, OutpostError, recoveryDetails } from "@elie-laloum/outpost";
import { coder, repository, sandboxProvider } from "./outpost.config.ts";

try {
  const result = await dispatch({
    repository,
    sandboxProvider,
    agent: coder,
    branch: { mode: "named", name: "outpost/upgrade-deps" },
    brief: { text: "Upgrade the test dependencies and commit the change." },
  });
  if (result.retainedDirectory) reportValue("Kept:", result.retainedDirectory);
  // Example output: Kept: /project/.outpost/workspaces/…
} catch (error) {
  console.error(recoveryDetails(error));
  if (error instanceof OutpostError) console.error(error.code, error.details);
  throw error;
}

Two failures also name their location in error.details. Errors lists every code.

API reference: recoveryDetails.

When the run itself also failed, the synchronization error arrives inside an AggregateError.

A transfer holds two sides: previous, your checkout before the sandbox’s changes, and incoming, the sandbox’s changes. Restore one side into a new directory, never over your checkout.

npx outpost recovery inspect --repository /projects/app --git --locks --resources
Drag to move · Ctrl + scroll to zoom
100 %
  • LookNothing is changed.
    1. InspectList workspaces, locks and recorded sandbox activity. recovery inspect
    2. VerifyCheck the transfer’s files, checksums and Git history. recovery verify
    (Steps)
    • → Restore : then
  • RestoreRebuild one side in a new directory.
    1. PlanPreview the commit and files to restore. recovery restore
    2. ApplyCreate a detached checkout from the plan. --apply
    (Steps)
    • → Integrate : then
  • IntegrateYou decide what comes back.
    1. CompareReview the restored checkout against your repository. git
    2. Bring backCommit, cherry-pick or merge the parts you keep. git
    (Steps)

API reference: RecoveryInspectionOptions.

The command exits with status 1 when the inventory is incomplete.

npx outpost recovery verify --directory "$TRANSFER" --checksums --restorability --repository /projects/app

$TRANSFER is the directory from details.recovery. --checksums compares each file with the transfer’s manifest; --max-bytes bounds the bytes hashed. --restorability rebuilds the commits and patches in a temporary clone of --repository. The command exits with status 1 when a check fails.

npx outpost recovery restore --directory "$TRANSFER" --repository /projects/app \
  --destination /projects/app-recovered --side incoming

This prints the plan. Run it again with --apply to create the checkout: a clone of your repository detached at the restored commit, with the side’s patches and files applied and no origin remote.

API reference: RecoveryRestoreOptions.

The destination must not exist and must be outside the repository, its Git metadata and the transfer. The transfer stays in place.

git -C /projects/app-recovered status
git -C /projects/app-recovered switch -c recovered
git -C /projects/app-recovered add -A
git -C /projects/app-recovered commit -m "Recover sandbox changes"
git -C /projects/app fetch /projects/app-recovered recovered:outpost/recovered

The work is now the outpost/recovered branch of your repository. Review it and merge it like any other branch.

Each command has a function. planRecoveryRestore() returns the plan; restoreRecoveryTransfer() checks that nothing changed since and applies it.

export const repository = "/projects/app";
export const transfer = process.env.TRANSFER!;
import { reportValue } from "./reporter.ts";
import { inspectRecovery, verifyRecoveryTransfer } from "@elie-laloum/outpost";
import { repository, transfer } from "./recovery-target.ts";

export async function verifyTransfer() {
  const inventory = await inspectRecovery({
    repository,
    git: true,
    locks: true,
  });
  reportValue(inventory.git?.workspaces);
  // Example output: [ { branch: "outpost/fix-tests", … } ]
  const verification = await verifyRecoveryTransfer(transfer, {
    checksums: true,
    restorability: true,
    repository,
  });
  if (!verification.complete)
    throw new Error("The transfer failed verification");
}
import { reportValue } from "./reporter.ts";
import { verifyTransfer } from "./verify-transfer.ts";
import {
  planRecoveryRestore,
  restoreRecoveryTransfer,
} from "@elie-laloum/outpost";
import { transfer, repository } from "./recovery-target.ts";

await verifyTransfer();
export const plan = await planRecoveryRestore({
  directory: transfer,
  repository,
  destination: "/projects/app-recovered",
  side: "incoming",
});
export const restored = await restoreRecoveryTransfer(plan);
reportValue(restored.directory, restored.commit);
// Example output: /project/.outpost/workspaces/… 8f3a21c…

inspectRecovery({ transporter }) lists the objects of a transport instead of a local repository.

archiveRecovery() verifies a transfer and uploads it through a transport. materializeRecoveryArchive() downloads it on any machine and checks its checksums again.

import { reportValue } from "./reporter.ts";
import {
  archiveRecovery,
  createLocalTransport,
  materializeRecoveryArchive,
} from "@elie-laloum/outpost";
const transporter = createLocalTransport({ directory: "/mnt/shared/outpost" });
const reference = await archiveRecovery({
  transporter,
  directory: process.env.TRANSFER!,
});
const staging = await materializeRecoveryArchive({
  transporter,
  reference,
  destination: "/projects/transfer-copy",
});
reportValue(staging);
// Example output: /project/.outpost/recovery/run-1

Keep reference (a key and a revision) to find the archive. Pass staging as --directory to outpost recovery restore, with a clone of the source repository.

A crashed workflow or candidate race keeps ownership of its checkpoint. After stopping the old process, release it with recoverWorkflowCheckpoint() (Durable runs) or recoverSpeculation() (Competing candidates).

  • Checksums detect damage against an unsigned manifest; they do not prove who produced the transfer.
  • Restorability covers commits, the bundle and patches, not submodules or external dependencies.
  • A transfer is restorable only once the host backup ran: a synchronization that failed during download or validation leaves no state.json, and the restore plan rejects it.
  • A lock PID or recorded activity is an observation. It does not prove that a remote process has stopped.
  • A worktree reported clean can still hold ignored files, such as copies or node_modules.
  • An archive holds recovery files, not the repository: restoring still needs the source repository.

API: recoveryDetails · inspectRecovery · verifyRecoveryTransfer · planRecoveryRestore · restoreRecoveryTransfer · archiveRecovery · materializeRecoveryArchive.