

























@@ -113,6 +113,16 @@ vi.mock("../plugins/provider-runtime.js", async () => {
113113 ...context.messages,
114114];
115115}
116+if (provider === "replay-poison") {
117+return context.messages.filter(
118+(message) =>
119+message.role !== "toolResult" ||
120+!(
121+(message as { isError?: unknown }).isError === true &&
122+JSON.stringify((message as { content?: unknown }).content).includes("aborted")
123+),
124+);
125+}
116126return context.messages;
117127},
118128),
@@ -741,6 +751,142 @@ describe("sanitizeSessionHistory", () => {
741751expect(JSON.stringify(result)).not.toContain("missing tool result");
742752});
743753754+it("keeps OpenAI Responses real tool results paired when strict id sanitization rewrites aliases", async () => {
755+const messages = castAgentMessages([
756+makeUserMessage("generate"),
757+makeAssistantMessage(
758+[
759+{
760+type: "toolCall",
761+id: "call_mock_image_generate_1",
762+name: "image_generate",
763+arguments: { prompt: "QA lighthouse" },
764+},
765+],
766+{ stopReason: "toolUse" },
767+),
768+{
769+role: "toolResult",
770+call_id: "call_mock_image_generate_1",
771+toolName: "image_generate",
772+content: [{ type: "text", text: "Background task started for image generation." }],
773+isError: false,
774+},
775+makeUserMessage("inspect the generated lighthouse attachment"),
776+]);
777+778+const result = await sanitizeOpenAIHistory(messages);
779+const assistant = result[1] as Extract<AgentMessage, { role: "assistant" }>;
780+const toolCall = extractToolCallsFromAssistant(assistant)[0];
781+const toolResult = result[2] as Extract<AgentMessage, { role: "toolResult" }> & {
782+call_id?: string;
783+};
784+785+expect(result.map((message) => message.role)).toEqual([
786+"user",
787+"assistant",
788+"toolResult",
789+"user",
790+]);
791+expect(toolCall?.id).toBe("callmockimagegenerate1");
792+expect(toolResult.toolCallId).toBe(toolCall?.id);
793+expect(toolResult.call_id).toBe(toolCall?.id);
794+expect(toolResult.content).toEqual([
795+{ type: "text", text: "Background task started for image generation." },
796+]);
797+});
798+799+it("repairs OpenAI Responses async image replay before assistant follow-up text", async () => {
800+const messages = castAgentMessages([
801+makeUserMessage("Image generation check. Generate an image of a QA lighthouse."),
802+makeAssistantMessage(
803+[
804+{
805+type: "toolCall",
806+id: "callmockimagegenerate0b27d8fa84",
807+name: "image_generate",
808+arguments: { prompt: "QA lighthouse" },
809+},
810+],
811+{ stopReason: "toolUse" },
812+),
813+{
814+role: "toolResult",
815+toolCallId: "callmockimagegenerate0b27d8fa84",
816+toolName: "image_generate",
817+content: [{ type: "text", text: "Background task started for image generation." }],
818+isError: false,
819+},
820+{
821+role: "custom",
822+content: "Image generation started; wait for completion.",
823+},
824+makeUserMessage("The image is ready for the original chat."),
825+makeAssistantMessage(
826+[
827+{
828+type: "toolCall",
829+id: "callmockimagegenerate0b27d8fa842",
830+name: "image_generate",
831+arguments: { prompt: "QA lighthouse" },
832+},
833+],
834+{ stopReason: "toolUse" },
835+),
836+makeAssistantMessage([{ type: "text", text: "Worked: the QA lighthouse image completed." }], {
837+stopReason: "stop",
838+}),
839+]);
840+841+const result = await sanitizeOpenAIHistory(messages);
842+843+expect(result.map((message) => message.role)).toEqual([
844+"user",
845+"assistant",
846+"toolResult",
847+"custom",
848+"user",
849+"assistant",
850+"toolResult",
851+"assistant",
852+]);
853+const repairedAssistant = result[5] as Extract<AgentMessage, { role: "assistant" }>;
854+const repairedToolCall = extractToolCallsFromAssistant(repairedAssistant)[0];
855+expect(result[6]).toMatchObject({
856+role: "toolResult",
857+toolCallId: repairedToolCall?.id,
858+toolName: "image_generate",
859+isError: true,
860+content: [{ type: "text", text: "aborted" }],
861+});
862+});
863+864+it("repairs OpenAI Responses replay again after provider hooks mutate history", async () => {
865+const messages = castAgentMessages([
866+makeUserMessage("generate"),
867+makeAssistantMessage(
868+[{ type: "toolCall", id: "call_1", name: "image_generate", arguments: {} }],
869+{ stopReason: "toolUse" },
870+),
871+makeAssistantMessage([{ type: "text", text: "done" }], { stopReason: "stop" }),
872+]);
873+874+const result = await sanitizeOpenAIHistory(messages, { provider: "replay-poison" });
875+876+expect(result.map((message) => message.role)).toEqual([
877+"user",
878+"assistant",
879+"toolResult",
880+"assistant",
881+]);
882+expect(result[2]).toMatchObject({
883+role: "toolResult",
884+toolName: "image_generate",
885+isError: true,
886+content: [{ type: "text", text: "aborted" }],
887+});
888+});
889+744890it("repairs a message-tool delivery-mirror poisoned OpenAI Responses replay", async () => {
745891const messages: AgentMessage[] = [
746892makeUserMessage("start"),
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。