Files
PaperClipAI/packages/adapter-utils/src/workspace-restore-teardown.ts
T
Devin FoleyandPaperclip 3b47a6befd Record bounded workspace restore failure stages (#15005)
Capture allowlisted restore substep and failure metadata for future diagnosis while preserving workspace recovery behavior, error identity, cleanup ordering, and privacy.

Validation: exact-head Greptile 5/5, passing CI, no unresolved review threads, and a clean merge. Detailed verification and limitations are recorded in the pull request.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-05 10:58:32 -07:00

54 lines
2.4 KiB
TypeScript

import type { RuntimeProgressSink } from "./runtime-progress.js";
import { getWorkspaceRestoreDiagnostic, withWorkspaceRestoreDiagnosticCapture } from "./workspace-restore-diagnostics.js";
import {
classifyWorkspaceRestoreFailure,
describeWorkspaceRestoreFailure,
type WorkspaceRestoreOutcome,
} from "./workspace-restore-merge.js";
/**
* The one staged-runtime capability the teardown factory needs: restore the
* sandbox workspace back onto the host.
*/
export interface WorkspaceRestoreTeardownRuntime {
restoreWorkspace(onProgress?: RuntimeProgressSink): Promise<void>;
}
/**
* Builds the shared ACP adapter teardown step. The Claude, Codex, and Gemini
* adapters run the same five steps at teardown and differ only in two log
* strings: the start message and the failure prefix. Give this factory the
* staged runtime, the `onLog` sink, and those two strings; it returns the
* teardown step.
*
* The returned step logs `startMessage` to `stdout`, restores the workspace,
* and on success returns `{ ok: true }`. On a caught error, it classifies the
* error into an allowlisted {@link WorkspaceRestoreOutcome} code, logs
* `failurePrefix` plus the allowlisted diagnostic to `stderr`, and returns
* `{ ok: false, code }`.
*/
export function createWorkspaceRestoreTeardown(input: {
stagedRuntime: WorkspaceRestoreTeardownRuntime;
onLog: (stream: "stdout" | "stderr", chunk: string) => Promise<void>;
startMessage: string;
failurePrefix: string;
}): () => Promise<WorkspaceRestoreOutcome> {
const { stagedRuntime, onLog, startMessage, failurePrefix } = input;
return () => withWorkspaceRestoreDiagnosticCapture(async () => {
try {
await onLog("stdout", startMessage);
await stagedRuntime.restoreWorkspace((line) => onLog("stdout", line));
return { ok: true };
} catch (err) {
// The run log is readable by any same-company actor, so it must never
// carry the caught error's own message: that message can hold a host
// filesystem path or a process id. Log only the fixed, allowlisted
// diagnostic for the classified code.
const code = classifyWorkspaceRestoreFailure(err);
const diagnostic = getWorkspaceRestoreDiagnostic(err);
await onLog("stderr", `${failurePrefix}: ${describeWorkspaceRestoreFailure(code)}\n`);
return { ok: false, code, ...(diagnostic ? { diagnostic } : {}) };
}
});
}