diff --git a/open-sse/executors/chatgpt-web.ts b/open-sse/executors/chatgpt-web.ts index 673495449d..9a4eb3cf36 100644 --- a/open-sse/executors/chatgpt-web.ts +++ b/open-sse/executors/chatgpt-web.ts @@ -649,28 +649,37 @@ function buildConversationBody( conversationId: string | null, parentMessageId: string ): Record { - const messages: ChatGptMessage[] = []; + // Critical: do NOT send prior turns as separate `assistant` and `user` + // messages in the `messages` array. ChatGPT's web API ("action: next") + // treats those as in-progress turns and the model will literally CONTINUE + // a prior assistant response in the new generation — observed as + // `[1] -> [12] -> [1123]` across three turns. + // + // Instead, fold all prior history into the system message and send only + // the current user message as a single new turn. The model then sees a + // single prompt with full context and responds fresh. + const systemParts: string[] = []; + if (parsed.systemMsg.trim()) { + systemParts.push(parsed.systemMsg.trim()); + } + if (parsed.history.length > 0 && !conversationId) { + const formatted = parsed.history + .map((h) => `${h.role === "assistant" ? "Assistant" : "User"}: ${h.content}`) + .join("\n\n"); + systemParts.push( + `Prior conversation (for context — answer only the new user message below):\n\n${formatted}` + ); + } - if (parsed.systemMsg.trim() && !conversationId) { + const messages: ChatGptMessage[] = []; + if (systemParts.length > 0 && !conversationId) { messages.push({ id: randomUUID(), author: { role: "system" }, - content: { content_type: "text", parts: [parsed.systemMsg.trim()] }, + content: { content_type: "text", parts: [systemParts.join("\n\n")] }, }); } - // Replay history only on a brand-new conversation; ChatGPT remembers prior turns - // server-side once a conversation_id exists. - if (!conversationId) { - for (const h of parsed.history) { - messages.push({ - id: randomUUID(), - author: { role: h.role }, - content: { content_type: "text", parts: [h.content] }, - }); - } - } - messages.push({ id: randomUUID(), author: { role: "user" }, diff --git a/tests/unit/chatgpt-web.test.ts b/tests/unit/chatgpt-web.test.ts index 081e9b42fe..1f1ec730b3 100644 --- a/tests/unit/chatgpt-web.test.ts +++ b/tests/unit/chatgpt-web.test.ts @@ -805,9 +805,15 @@ test("Session continuity: each call starts a fresh conversation (Temporary Chat assert.equal(convIndices.length, 2); const secondBody = JSON.parse(m.calls.bodies[convIndices[1]]); assert.equal(secondBody.conversation_id, null, "should start a fresh conversation"); - // The full history should be replayed in the messages array. + // History is folded into the system message (so the model doesn't try to + // continue prior assistant turns); only the latest user message is sent. const userMessages = secondBody.messages.filter((m) => m.author?.role === "user"); - assert.equal(userMessages.length, 2, "should include First question + Follow-up"); + assert.equal(userMessages.length, 1, "only the latest user message is in the messages array"); + assert.equal(userMessages[0].content.parts[0], "Follow-up"); + const systemMsg = secondBody.messages.find((m) => m.author?.role === "system"); + assert.ok(systemMsg, "history should be packaged in a system message"); + assert.match(systemMsg.content.parts[0], /First question/); + assert.match(systemMsg.content.parts[0], /Hello, world!/); } finally { m.restore(); } @@ -864,9 +870,10 @@ test("Request: payload has correct ChatGPT shape", async () => { assert.equal(body.action, "next"); assert.equal(body.model, "gpt-5-3"); assert.equal(body.history_and_training_disabled, true); - // System + user in messages + // System message preserves the user-supplied system prompt; the user + // message is the latest query. assert.equal(body.messages[0].author.role, "system"); - assert.equal(body.messages[0].content.parts[0], "Be concise"); + assert.match(body.messages[0].content.parts[0], /Be concise/); assert.equal(body.messages[body.messages.length - 1].author.role, "user"); assert.equal(body.messages[body.messages.length - 1].content.parts[0], "What is 2+2?"); } finally {