From 85654d4c5c50fc13c3b1ec5b2f1f30daee34facf Mon Sep 17 00:00:00 2001 From: Dotta Date: Sun, 27 Sep 2026 19:34:46 -0500 Subject: [PATCH] fix(grok): distinguish instruction collection from workspace restore Collect in the execution boundary before the separate restoration callback. A collection rejection still restores and cleans up, but retains its own error instead of requesting workspace recovery. Co-Authored-By: Paperclip --- .../grok-local/src/server/execute.test.ts | 27 +++++++++++++++++++ .../adapters/grok-local/src/server/execute.ts | 17 +++++++----- 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/packages/adapters/grok-local/src/server/execute.test.ts b/packages/adapters/grok-local/src/server/execute.test.ts index f3552bc06f6..f24c0a74f80 100644 --- a/packages/adapters/grok-local/src/server/execute.test.ts +++ b/packages/adapters/grok-local/src/server/execute.test.ts @@ -847,6 +847,33 @@ describe("grok_local execute", () => { expect(await pathExists(stagedDir)).toBe(false); }); + it("keeps collection failure separate from a successful workspace restore", async () => { + delete process.env.XAI_API_KEY; + mocks.state.isRemote = true; + await seedHostGrokAuth("{}"); + const collectionError = new Error("instruction collection failed"); + const order: string[] = []; + let stagedDir = ""; + runProcessMock.mockImplementation(async (_run, _target, _command, _args, options) => { + options.onProcessStopped(); + return makeSuccessfulRunResult(); + }); + prepareRuntimeMock.mockImplementationOnce(async (input: { assets?: Array<{ localDir: string }> }) => { + stagedDir = input.assets?.[0]?.localDir ?? ""; + return { + workspaceRemoteDir: "/remote/workspace", + assetDirs: { home: "/remote/workspace/.paperclip-runtime/grok/home" }, + restoreWorkspace: async () => { order.push("restore"); }, + }; + }); + const ctx = await makeCtx("run-collection-reject", await makeTempRoot()); + ctx.onProviderStopped = async () => { order.push("collect"); throw collectionError; }; + await expect(execute(ctx)).rejects.toBe(collectionError); + expect(order).toEqual(["collect", "restore"]); + expect(stagedDir).not.toBe(""); + expect(await pathExists(stagedDir)).toBe(false); + }); + it.each(["completed", "failed", "timed_out"])("preserves %s output and removes the staged home when restore fails", async (state) => { delete process.env.XAI_API_KEY; mocks.state.isRemote = true; diff --git a/packages/adapters/grok-local/src/server/execute.ts b/packages/adapters/grok-local/src/server/execute.ts index 996fb7ef8ed..67490b93c33 100644 --- a/packages/adapters/grok-local/src/server/execute.ts +++ b/packages/adapters/grok-local/src/server/execute.ts @@ -728,13 +728,16 @@ async function executeTurn(ctx: AdapterExecutionContext): Promise { - try { - await providerStop.collectBeforeRestore(); - } finally { - await restoreRemoteWorkspace?.(); - } - }); + return await withWorkspaceRestore( + async () => { + try { + return await executeTurn(); + } finally { + await providerStop.collectBeforeRestore(); + } + }, + async () => { await restoreRemoteWorkspace?.(); }, + ); } finally { // Cleanup runs after settlement on both success and failure. if (stagedGrokHomeDir) {