Browse Source

Remove GPT‑5 instructions/reasoning_summary from UI message metadata to prevent ui_messages.json bloat (#8756)

chore(gpt5): stop persisting instructions/reasoning_summary in UI message metadata

Problem: ui_messages.json was getting bloated with unused or duplicated content (system 'instructions' and 'reasoning_summary') that we do not read back. Root cause: earlier OpenAI Responses API implementation persisted these fields to per-message metadata; however, 'instructions' are already sent as top-level request instructions and 'reasoning_summary' is surfaced live via streaming events. Neither field is consumed from storage. Changes: (1) Task.persistGpt5Metadata now stores only previous_response_id; (2) removed instructions and reasoning_summary from types; (3) updated Zod schema; (4) persistence layer writes messages as-is (no sanitizer); (5) tests green. Impact: smaller ui_messages.json, no runtime behavior change for requests. Migration: old metadata fields will be ignored by schema.
Hannes Rudolph 2 months ago
parent
commit
6ffdd440ce

+ 0 - 2
packages/types/src/message.ts

@@ -221,8 +221,6 @@ export const clineMessageSchema = z.object({
 			gpt5: z
 				.object({
 					previous_response_id: z.string().optional(),
-					instructions: z.string().optional(),
-					reasoning_summary: z.string().optional(),
 				})
 				.optional(),
 		})

+ 71 - 0
src/core/task-persistence/__tests__/taskMessages.spec.ts

@@ -0,0 +1,71 @@
+import { describe, it, expect, vi, beforeEach } from "vitest"
+import * as os from "os"
+import * as path from "path"
+import * as fs from "fs/promises"
+
+// Mocks (use hoisted to avoid initialization ordering issues)
+const hoisted = vi.hoisted(() => ({
+	safeWriteJsonMock: vi.fn().mockResolvedValue(undefined),
+}))
+vi.mock("../../../utils/safeWriteJson", () => ({
+	safeWriteJson: hoisted.safeWriteJsonMock,
+}))
+
+// Import after mocks
+import { saveTaskMessages } from "../taskMessages"
+
+let tmpBaseDir: string
+
+beforeEach(async () => {
+	hoisted.safeWriteJsonMock.mockClear()
+	// Create a unique, writable temp directory to act as globalStoragePath
+	tmpBaseDir = await fs.mkdtemp(path.join(os.tmpdir(), "roo-test-"))
+})
+
+describe("taskMessages.saveTaskMessages", () => {
+	beforeEach(() => {
+		hoisted.safeWriteJsonMock.mockClear()
+	})
+
+	it("persists messages as-is", async () => {
+		const messages: any[] = [
+			{
+				role: "assistant",
+				content: "Hello",
+				metadata: {
+					gpt5: {
+						previous_response_id: "resp_123",
+					},
+					other: "keep",
+				},
+			},
+			{ role: "user", content: "Do thing" },
+		]
+
+		await saveTaskMessages({
+			messages,
+			taskId: "task-1",
+			globalStoragePath: tmpBaseDir,
+		})
+
+		expect(hoisted.safeWriteJsonMock).toHaveBeenCalledTimes(1)
+		const [, persisted] = hoisted.safeWriteJsonMock.mock.calls[0]
+		expect(persisted).toEqual(messages)
+	})
+
+	it("persists messages without modification when no metadata", async () => {
+		const messages: any[] = [
+			{ role: "assistant", content: "Hi" },
+			{ role: "user", content: "Yo" },
+		]
+
+		await saveTaskMessages({
+			messages,
+			taskId: "task-2",
+			globalStoragePath: tmpBaseDir,
+		})
+
+		const [, persisted] = hoisted.safeWriteJsonMock.mock.calls[0]
+		expect(persisted).toEqual(messages)
+	})
+})

+ 6 - 6
src/core/task/Task.ts

@@ -2267,7 +2267,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
 					}
 				}
 
-				await this.persistGpt5Metadata(reasoningMessage)
+				await this.persistGpt5Metadata()
 				await this.saveClineMessages()
 				await this.providerRef.deref()?.postStateToWebview()
 
@@ -2853,10 +2853,12 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
 	}
 
 	/**
-	 * Persist GPT-5 per-turn metadata (previous_response_id, instructions, reasoning_summary)
+	 * Persist GPT-5 per-turn metadata (previous_response_id only)
 	 * onto the last complete assistant say("text") message.
+	 *
+	 * Note: We do not persist system instructions or reasoning summaries.
 	 */
-	private async persistGpt5Metadata(reasoningMessage?: string): Promise<void> {
+	private async persistGpt5Metadata(): Promise<void> {
 		try {
 			const modelId = this.api.getModel().id
 			if (!modelId || !modelId.startsWith("gpt-5")) return
@@ -2875,9 +2877,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
 				}
 				const gpt5Metadata: Gpt5Metadata = {
 					...(msg.metadata.gpt5 ?? {}),
-					previous_response_id: lastResponseId,
-					instructions: this.lastUsedInstructions,
-					reasoning_summary: (reasoningMessage ?? "").trim() || undefined,
+					...(lastResponseId ? { previous_response_id: lastResponseId } : {}),
 				}
 				msg.metadata.gpt5 = gpt5Metadata
 			}

+ 0 - 12
src/core/task/types.ts

@@ -12,18 +12,6 @@ export interface Gpt5Metadata {
 	 * Used to maintain conversation continuity in subsequent requests
 	 */
 	previous_response_id?: string
-
-	/**
-	 * The system instructions/prompt used for this response
-	 * Stored to track what instructions were active when the response was generated
-	 */
-	instructions?: string
-
-	/**
-	 * The reasoning summary from GPT-5's reasoning process
-	 * Contains the model's internal reasoning if reasoning mode was enabled
-	 */
-	reasoning_summary?: string
 }
 
 /**