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?.(); }, );