mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-10 12:37:55 +01:00
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>
74 lines
3.0 KiB
TypeScript
74 lines
3.0 KiB
TypeScript
import { hasWorkspaceRestoreFailure, safeWorkspaceRestorePath } from "@paperclipai/shared";
|
|
import type { AdapterExecutionResult } from "./types.js";
|
|
import { classifyWorkspaceRestoreFailure } from "./workspace-restore-merge.js";
|
|
import { getWorkspaceRestoreDiagnostic, withWorkspaceRestoreDiagnosticCapture } from "./workspace-restore-diagnostics.js";
|
|
|
|
/** A completed model turn does not imply that required workspace files arrived. */
|
|
export function applyWorkspaceRestoreFailure(result: AdapterExecutionResult): AdapterExecutionResult {
|
|
if (!hasWorkspaceRestoreFailure(result.resultJson) || result.errorCode === "workspace_restore_failed") return result;
|
|
return {
|
|
...result,
|
|
timedOut: false,
|
|
errorCode: "workspace_restore_failed",
|
|
errorMessage: [result.errorMessage, "Workspace restore failed. Workspace files need recovery."].filter(Boolean).join(" "),
|
|
resultJson: {
|
|
...result.resultJson,
|
|
executionBeforeRestore: {
|
|
errorCode: result.errorCode ?? null,
|
|
exitCode: result.exitCode,
|
|
signal: result.signal,
|
|
timedOut: result.timedOut,
|
|
},
|
|
finalResponseRecorded: typeof result.resultJson?.finalResponseRecorded === "boolean"
|
|
? result.resultJson.finalResponseRecorded : Boolean(result.summary?.trim()),
|
|
},
|
|
};
|
|
}
|
|
|
|
/** Preserve a pending result (or earlier error) when copy-back fails. */
|
|
export async function withWorkspaceRestore(
|
|
execute: () => Promise<AdapterExecutionResult>,
|
|
restore: () => Promise<void>,
|
|
): Promise<AdapterExecutionResult> {
|
|
return withWorkspaceRestoreDiagnosticCapture(async () => {
|
|
let result: AdapterExecutionResult | undefined;
|
|
let executionError: unknown;
|
|
let executionThrew = false;
|
|
try {
|
|
result = await execute();
|
|
} catch (error) {
|
|
executionError = error;
|
|
executionThrew = true;
|
|
}
|
|
try {
|
|
await restore();
|
|
} catch (error) {
|
|
const code = classifyWorkspaceRestoreFailure(error);
|
|
const diagnostic = getWorkspaceRestoreDiagnostic(error);
|
|
// Parse the archive member only. Never expose the unsafe link target.
|
|
const member = error instanceof Error
|
|
? /Daytona syncOut refusing tarball link whose target escapes the extraction dir: (.+?) -> /.exec(error.message)?.[1]
|
|
: null;
|
|
const relativePath = safeWorkspaceRestorePath(member);
|
|
return applyWorkspaceRestoreFailure({
|
|
...(result ?? {
|
|
exitCode: null,
|
|
signal: null,
|
|
timedOut: false,
|
|
errorCode: "adapter_failed",
|
|
// The server applies its ordinary execution-error redaction to this field.
|
|
errorMessage: executionError instanceof Error ? executionError.message : "Adapter execution failed.",
|
|
}),
|
|
resultJson: {
|
|
...result?.resultJson,
|
|
workspaceRestoreFailure: code,
|
|
...(diagnostic ? { workspaceRestoreDiagnostic: diagnostic } : {}),
|
|
...(relativePath ? { workspaceRestorePath: relativePath } : {}),
|
|
},
|
|
});
|
|
}
|
|
if (executionThrew) throw executionError;
|
|
return result!;
|
|
});
|
|
}
|