From 427a3cabf28e26c70267fe79cbd063533b7d3648 Mon Sep 17 00:00:00 2001 From: Dotta Date: Sun, 27 Sep 2026 19:42:22 -0500 Subject: [PATCH] fix(grok): retain provider results across collection and restore failures Record collection failure separately on the completed result before workspace restoration. Keep the provider exit, session, usage, and output when both cleanup steps fail, while keeping raw collector errors out of logs. Co-Authored-By: Paperclip --- .../grok-local/src/server/execute.test.ts | 38 ++++++++++++++++++- .../adapters/grok-local/src/server/execute.ts | 21 +++++++++- 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/packages/adapters/grok-local/src/server/execute.test.ts b/packages/adapters/grok-local/src/server/execute.test.ts index f24c0a74f80..1553e7aa8a4 100644 --- a/packages/adapters/grok-local/src/server/execute.test.ts +++ b/packages/adapters/grok-local/src/server/execute.test.ts @@ -868,12 +868,48 @@ describe("grok_local execute", () => { }); const ctx = await makeCtx("run-collection-reject", await makeTempRoot()); ctx.onProviderStopped = async () => { order.push("collect"); throw collectionError; }; - await expect(execute(ctx)).rejects.toBe(collectionError); + const result = await execute(ctx); + expect(result.errorCode).toBe("instruction_collection_failed"); + expect(result.resultJson?.instructionCollectionFailure).toBe("collection_failed"); + expect(result.resultJson?.workspaceRestoreFailure).toBeUndefined(); expect(order).toEqual(["collect", "restore"]); expect(stagedDir).not.toBe(""); expect(await pathExists(stagedDir)).toBe(false); }); + it.each([0, 2])("preserves provider output when collection and restore both fail after exit %s", async (exitCode) => { + delete process.env.XAI_API_KEY; + mocks.state.isRemote = true; + await seedHostGrokAuth("{}"); + runProcessMock.mockImplementation(async (_run, _target, _command, _args, options) => { + options.onProcessStopped(); + return { + ...makeSuccessfulRunResult(), exitCode, + stderr: exitCode ? "Model request failed." : "", + stdout: [JSON.stringify({ type: "text", data: "Saved output." }), JSON.stringify({ + type: "end", sessionId: "sess-1", requestId: "req-1", stopReason: "EndTurn", + usage: { input_tokens: 4, output_tokens: 9 }, + })].join("\n"), + }; + }); + prepareRuntimeMock.mockImplementationOnce(async () => ({ + workspaceRemoteDir: "/remote/workspace", + assetDirs: { home: "/remote/workspace/.paperclip-runtime/grok/home" }, + restoreWorkspace: async () => { throw new Error("restore failed"); }, + })); + const ctx = await makeCtx("run-collection-and-restore-reject", await makeTempRoot()); + ctx.onProviderStopped = async () => { throw new Error("instruction collection failed"); }; + const result = await execute(ctx); + expect(result).toMatchObject({ + errorCode: "workspace_restore_failed", exitCode, sessionId: "sess-1", summary: "Saved output.", + usage: { inputTokens: 4, outputTokens: 9 }, + resultJson: { instructionCollectionFailure: "collection_failed", workspaceRestoreFailure: "restore_failed", + requestId: "req-1", finalResponseRecorded: true, executionBeforeRestore: { exitCode } }, + }); + expect(result.errorMessage).toContain("Instruction collection failed"); + if (exitCode) expect(result.errorMessage).toContain("Model request failed."); + }); + 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 67490b93c33..fab47a1fb6e 100644 --- a/packages/adapters/grok-local/src/server/execute.ts +++ b/packages/adapters/grok-local/src/server/execute.ts @@ -730,11 +730,28 @@ async function executeTurn(ctx: AdapterExecutionContext): Promise { + let result: AdapterExecutionResult; + let collectionFailed = false; + const collectionFailureMessage = "Instruction collection failed after provider stop. No instruction save is claimed."; try { - return await executeTurn(); + result = await executeTurn(); } finally { - await providerStop.collectBeforeRestore(); + try { + await providerStop.collectBeforeRestore(); + } catch { + collectionFailed = true; + await onLog("stderr", `[paperclip] ${collectionFailureMessage}\n`).catch(() => undefined); + } } + if (!collectionFailed) return result; + const providerFailed = result.timedOut || result.signal || result.errorCode + || (result.exitCode !== null && result.exitCode !== 0); + return { + ...result, + ...(!providerFailed ? { errorCode: "instruction_collection_failed" } : {}), + errorMessage: [result.errorMessage, collectionFailureMessage].filter(Boolean).join(" "), + resultJson: { ...result.resultJson, instructionCollectionFailure: "collection_failed" }, + }; }, async () => { await restoreRemoteWorkspace?.(); }, );