Skip to content

Commit ebcaad8

Browse files
committed
Migrate conversation continuity to plugin-side encrypted reasoning items (Responses API)
Summary We moved continuity off OpenAI servers and now maintain conversation state locally by persisting and replaying encrypted reasoning items. Requests are stateless (store=false) while retaining the performance/caching benefits of the Responses API. Why This aligns with how Roo manages context and simplifies our Responses API implementation while keeping all the benefits of continuity, caching, and latency improvements. What changed - All OpenAI models now use the Responses API; system instructions are passed via the top-level instructions field; requests include store=false and include=["reasoning.encrypted_content"]. - We persist encrypted reasoning items (type: "reasoning", encrypted_content, optional id) into API history and replay them on subsequent turns. - Reasoning summaries default to summary: "auto" when supported; text.verbosity only when supported. - Atomic persistence via safeWriteJson. Removed - previous_response_id flows, suppressPreviousResponseId/skipPrevResponseIdOnce, persistGpt5Metadata(), and GPT‑5 response ID metadata in UI messages. Kept - taskId and mode metadata for cross-provider features. Result - ZDR-friendly, stateless continuity with equal or better performance and a simpler codepath.
1 parent 69d4efc commit ebcaad8

File tree

9 files changed

+166
-532
lines changed

9 files changed

+166
-532
lines changed

packages/types/src/message.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -226,15 +226,6 @@ export const clineMessageSchema = z.object({
226226
isProtected: z.boolean().optional(),
227227
apiProtocol: z.union([z.literal("openai"), z.literal("anthropic")]).optional(),
228228
isAnswered: z.boolean().optional(),
229-
metadata: z
230-
.object({
231-
gpt5: z
232-
.object({
233-
previous_response_id: z.string().optional(),
234-
})
235-
.optional(),
236-
})
237-
.optional(),
238229
})
239230

240231
export type ClineMessage = z.infer<typeof clineMessageSchema>

src/api/index.ts

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -49,23 +49,20 @@ export interface SingleCompletionHandler {
4949
}
5050

5151
export interface ApiHandlerCreateMessageMetadata {
52-
mode?: string
53-
taskId: string
54-
previousResponseId?: string
5552
/**
56-
* When true, the provider must NOT fall back to internal continuity state
57-
* (e.g., lastResponseId) if previousResponseId is absent.
58-
* Used to enforce "skip once" after a condense operation.
53+
* Task ID used for tracking and provider-specific features:
54+
* - DeepInfra: Used as prompt_cache_key for caching
55+
* - Roo: Sent as X-Roo-Task-ID header
56+
* - Requesty: Sent as trace_id
57+
* - Unbound: Sent in unbound_metadata
5958
*/
60-
suppressPreviousResponseId?: boolean
59+
taskId: string
6160
/**
62-
* Controls whether the response should be stored for 30 days in OpenAI's Responses API.
63-
* When true (default), responses are stored and can be referenced in future requests
64-
* using the previous_response_id for efficient conversation continuity.
65-
* Set to false to opt out of response storage for privacy or compliance reasons.
66-
* @default true
61+
* Current mode slug for provider-specific tracking:
62+
* - Requesty: Sent in extra metadata
63+
* - Unbound: Sent in unbound_metadata
6764
*/
68-
store?: boolean
65+
mode?: string
6966
}
7067

7168
export interface ApiHandler {

src/api/providers/__tests__/openai-native.spec.ts

Lines changed: 32 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -686,69 +686,6 @@ describe("OpenAiNativeHandler", () => {
686686
expect(contentChunks).toHaveLength(0)
687687
})
688688

689-
it("should support previous_response_id for conversation continuity", async () => {
690-
// Mock fetch for Responses API
691-
const mockFetch = vitest.fn().mockResolvedValue({
692-
ok: true,
693-
body: new ReadableStream({
694-
start(controller) {
695-
// Include response ID in the response
696-
controller.enqueue(
697-
new TextEncoder().encode(
698-
'data: {"type":"response.created","response":{"id":"resp_123","status":"in_progress"}}\n\n',
699-
),
700-
)
701-
controller.enqueue(
702-
new TextEncoder().encode(
703-
'data: {"type":"response.output_item.added","item":{"type":"text","text":"Response with ID"}}\n\n',
704-
),
705-
)
706-
controller.enqueue(
707-
new TextEncoder().encode(
708-
'data: {"type":"response.done","response":{"id":"resp_123","usage":{"prompt_tokens":10,"completion_tokens":3}}}\n\n',
709-
),
710-
)
711-
controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n"))
712-
controller.close()
713-
},
714-
}),
715-
})
716-
global.fetch = mockFetch as any
717-
718-
// Mock SDK to fail
719-
mockResponsesCreate.mockRejectedValue(new Error("SDK not available"))
720-
721-
handler = new OpenAiNativeHandler({
722-
...mockOptions,
723-
apiModelId: "gpt-5-2025-08-07",
724-
})
725-
726-
// First request - should not have previous_response_id
727-
const stream1 = handler.createMessage(systemPrompt, messages)
728-
const chunks1: any[] = []
729-
for await (const chunk of stream1) {
730-
chunks1.push(chunk)
731-
}
732-
733-
// Verify first request doesn't include previous_response_id
734-
let firstCallBody = JSON.parse(mockFetch.mock.calls[0][1].body)
735-
expect(firstCallBody.previous_response_id).toBeUndefined()
736-
737-
// Second request with metadata - should include previous_response_id
738-
const stream2 = handler.createMessage(systemPrompt, messages, {
739-
taskId: "test-task",
740-
previousResponseId: "resp_456",
741-
})
742-
const chunks2: any[] = []
743-
for await (const chunk of stream2) {
744-
chunks2.push(chunk)
745-
}
746-
747-
// Verify second request includes the provided previous_response_id
748-
let secondCallBody = JSON.parse(mockFetch.mock.calls[1][1].body)
749-
expect(secondCallBody.previous_response_id).toBe("resp_456")
750-
})
751-
752689
it("should handle unhandled stream events gracefully", async () => {
753690
// Mock fetch for the fallback SSE path
754691
const mockFetch = vitest.fn().mockResolvedValue({
@@ -798,7 +735,7 @@ describe("OpenAiNativeHandler", () => {
798735
expect(textChunks[0].text).toBe("Hello")
799736
})
800737

801-
it("should use stored response ID when metadata doesn't provide one", async () => {
738+
it.skip("should use stored response ID when metadata doesn't provide one - DEPRECATED", async () => {
802739
// Mock fetch for Responses API
803740
const mockFetch = vitest
804741
.fn()
@@ -854,12 +791,10 @@ describe("OpenAiNativeHandler", () => {
854791
// consume stream
855792
}
856793

857-
// Verify second request uses the stored response ID from first request
858-
let secondCallBody = JSON.parse(mockFetch.mock.calls[1][1].body)
859-
expect(secondCallBody.previous_response_id).toBe("resp_789")
794+
// DEPRECATED: This test is for old previous_response_id behavior
860795
})
861796

862-
it("should retry with full conversation when previous_response_id fails", async () => {
797+
it.skip("should retry with full conversation when previous_response_id fails - DEPRECATED", async () => {
863798
// This test verifies the fix for context loss bug when previous_response_id becomes invalid
864799
const mockFetch = vitest
865800
.fn()
@@ -908,10 +843,9 @@ describe("OpenAiNativeHandler", () => {
908843
{ role: "user", content: "And 4+4?" }, // Latest message
909844
]
910845

911-
// Call with a previous_response_id that will fail
846+
// Call without previous_response_id
912847
const stream = handler.createMessage(systemPrompt, conversationMessages, {
913848
taskId: "test-task",
914-
previousResponseId: "resp_invalid",
915849
})
916850

917851
const chunks: any[] = []
@@ -966,7 +900,7 @@ describe("OpenAiNativeHandler", () => {
966900
])
967901
})
968902

969-
it("should retry with full conversation when SDK returns 400 for invalid previous_response_id", async () => {
903+
it.skip("should retry with full conversation when SDK returns 400 for invalid previous_response_id - DEPRECATED", async () => {
970904
// Test the SDK path (executeRequest method) for handling invalid previous_response_id
971905

972906
// Mock SDK to return an async iterable that we can control
@@ -1010,10 +944,9 @@ describe("OpenAiNativeHandler", () => {
1010944
{ role: "user", content: "What number did I ask you to remember?" },
1011945
]
1012946

1013-
// Call with a previous_response_id that will fail
947+
// Call without previous_response_id
1014948
const stream = handler.createMessage(systemPrompt, conversationMessages, {
1015949
taskId: "test-task",
1016-
previousResponseId: "resp_invalid",
1017950
})
1018951

1019952
const chunks: any[] = []
@@ -1061,7 +994,7 @@ describe("OpenAiNativeHandler", () => {
1061994
])
1062995
})
1063996

1064-
it("should only send latest message when using previous_response_id", async () => {
997+
it.skip("should only send latest message when using previous_response_id - DEPRECATED", async () => {
1065998
// Mock fetch for Responses API
1066999
const mockFetch = vitest
10671000
.fn()
@@ -1152,7 +1085,6 @@ describe("OpenAiNativeHandler", () => {
11521085

11531086
const stream2 = handler.createMessage(systemPrompt, secondMessages, {
11541087
taskId: "test-task",
1155-
previousResponseId: "resp_001",
11561088
})
11571089
for await (const chunk of stream2) {
11581090
// consume stream
@@ -1169,26 +1101,44 @@ describe("OpenAiNativeHandler", () => {
11691101
expect(secondCallBody.previous_response_id).toBe("resp_001")
11701102
})
11711103

1172-
it("should correctly prepare structured input", () => {
1104+
it("should format full conversation correctly", async () => {
1105+
const mockFetch = vitest.fn().mockResolvedValue({
1106+
ok: true,
1107+
body: new ReadableStream({
1108+
start(controller) {
1109+
controller.enqueue(
1110+
new TextEncoder().encode(
1111+
'data: {"type":"response.output_item.added","item":{"type":"text","text":"Response"}}\n\n',
1112+
),
1113+
)
1114+
controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n"))
1115+
controller.close()
1116+
},
1117+
}),
1118+
})
1119+
global.fetch = mockFetch as any
1120+
mockResponsesCreate.mockRejectedValue(new Error("SDK not available"))
1121+
11731122
const gpt5Handler = new OpenAiNativeHandler({
11741123
...mockOptions,
11751124
apiModelId: "gpt-5-2025-08-07",
11761125
})
11771126

1178-
// Test with metadata that has previousResponseId
1179-
// @ts-expect-error - private method
1180-
const { formattedInput, previousResponseId } = gpt5Handler.prepareStructuredInput(systemPrompt, messages, {
1127+
const stream = gpt5Handler.createMessage(systemPrompt, messages, {
11811128
taskId: "task1",
1182-
previousResponseId: "resp_123",
11831129
})
1130+
for await (const chunk of stream) {
1131+
// consume
1132+
}
11841133

1185-
expect(previousResponseId).toBe("resp_123")
1186-
expect(formattedInput).toEqual([
1134+
const callBody = JSON.parse(mockFetch.mock.calls[0][1].body)
1135+
expect(callBody.input).toEqual([
11871136
{
11881137
role: "user",
11891138
content: [{ type: "input_text", text: "Hello!" }],
11901139
},
11911140
])
1141+
expect(callBody.previous_response_id).toBeUndefined()
11921142
})
11931143

11941144
it("should provide helpful error messages for different error codes", async () => {

0 commit comments

Comments
 (0)