mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
refactor: rename ApiMessage to LegacyApiMessage and strip cache providerOptions on save
This commit is contained in:
parent
26899bf601
commit
b8c8749106
11 changed files with 293 additions and 49 deletions
|
|
@ -1,5 +1,5 @@
|
|||
import { describe, it, expect } from "vitest"
|
||||
import { ApiMessage } from "../../task-persistence/apiMessages"
|
||||
import { LegacyApiMessage } from "../../task-persistence/apiMessages"
|
||||
import { getEffectiveApiHistory, getMessagesSinceLastSummary } from "../index"
|
||||
|
||||
describe("nested condensing scenarios", () => {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
import { TelemetryService } from "@roo-code/telemetry"
|
||||
|
||||
import { getEffectiveApiHistory, cleanupAfterTruncation } from "../index"
|
||||
import { ApiMessage } from "../../task-persistence/apiMessages"
|
||||
import { LegacyApiMessage } from "../../task-persistence/apiMessages"
|
||||
|
||||
describe("Rewind After Condense - Issue #8295", () => {
|
||||
beforeEach(() => {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import * as path from "path"
|
||||
import { Task } from "../task/Task"
|
||||
import { ClineMessage } from "@roo-code/types"
|
||||
import { ApiMessage } from "../task-persistence/apiMessages"
|
||||
import { LegacyApiMessage } from "../task-persistence/apiMessages"
|
||||
import { cleanupAfterTruncation } from "../condense"
|
||||
import { OutputInterceptor } from "../../integrations/terminal/OutputInterceptor"
|
||||
import { getTaskDirectoryPath } from "../../utils/storage"
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ import * as os from "os"
|
|||
import * as path from "path"
|
||||
import * as fs from "fs/promises"
|
||||
|
||||
import { detectFormat, readRooMessages, saveRooMessages } from "../apiMessages"
|
||||
import type { ApiMessage } from "../apiMessages"
|
||||
import { detectFormat, readRooMessages, saveRooMessages, stripCacheProviderOptions } from "../apiMessages"
|
||||
import type { LegacyApiMessage } from "../apiMessages"
|
||||
import type { RooMessage, RooMessageHistory } from "../rooMessage"
|
||||
import { ROO_MESSAGE_VERSION } from "../rooMessage"
|
||||
import * as safeWriteJsonModule from "../../../utils/safeWriteJson"
|
||||
|
|
@ -46,7 +46,7 @@ const sampleV2Envelope: RooMessageHistory = {
|
|||
messages: sampleRooMessages,
|
||||
}
|
||||
|
||||
const sampleLegacyMessages: ApiMessage[] = [
|
||||
const sampleLegacyMessages: LegacyApiMessage[] = [
|
||||
{ role: "user", content: "Hello from legacy", ts: 1000 },
|
||||
{ role: "assistant", content: "Legacy response", ts: 2000 },
|
||||
]
|
||||
|
|
@ -275,3 +275,198 @@ describe("round-trip", () => {
|
|||
expect(detectFormat(parsed)).toBe("v2")
|
||||
})
|
||||
})
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// stripCacheProviderOptions
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe("stripCacheProviderOptions", () => {
|
||||
it("returns messages unchanged when they have no providerOptions", () => {
|
||||
const messages: RooMessage[] = [
|
||||
{ role: "user" as const, content: [{ type: "text" as const, text: "hi" }] },
|
||||
{ role: "assistant" as const, content: [{ type: "text" as const, text: "hello" }] },
|
||||
]
|
||||
|
||||
const result = stripCacheProviderOptions(messages)
|
||||
|
||||
expect(result).toEqual(messages)
|
||||
})
|
||||
|
||||
it("strips anthropic.cacheControl and removes empty providerOptions", () => {
|
||||
const messages: RooMessage[] = [
|
||||
{
|
||||
role: "assistant" as const,
|
||||
content: [{ type: "text" as const, text: "hello" }],
|
||||
providerOptions: {
|
||||
anthropic: { cacheControl: { type: "ephemeral" } },
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const result = stripCacheProviderOptions(messages)
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
role: "assistant" as const,
|
||||
content: [{ type: "text" as const, text: "hello" }],
|
||||
},
|
||||
])
|
||||
expect(result[0]).not.toHaveProperty("providerOptions")
|
||||
})
|
||||
|
||||
it("strips bedrock.cachePoint and removes empty providerOptions", () => {
|
||||
const messages: RooMessage[] = [
|
||||
{
|
||||
role: "assistant" as const,
|
||||
content: [{ type: "text" as const, text: "hello" }],
|
||||
providerOptions: {
|
||||
bedrock: { cachePoint: { type: "default" } },
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const result = stripCacheProviderOptions(messages)
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
role: "assistant" as const,
|
||||
content: [{ type: "text" as const, text: "hello" }],
|
||||
},
|
||||
])
|
||||
expect(result[0]).not.toHaveProperty("providerOptions")
|
||||
})
|
||||
|
||||
it("strips both anthropic.cacheControl and bedrock.cachePoint simultaneously", () => {
|
||||
const messages: RooMessage[] = [
|
||||
{
|
||||
role: "assistant" as const,
|
||||
content: [{ type: "text" as const, text: "hello" }],
|
||||
providerOptions: {
|
||||
anthropic: { cacheControl: { type: "ephemeral" } },
|
||||
bedrock: { cachePoint: { type: "default" } },
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const result = stripCacheProviderOptions(messages)
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
role: "assistant" as const,
|
||||
content: [{ type: "text" as const, text: "hello" }],
|
||||
},
|
||||
])
|
||||
expect(result[0]).not.toHaveProperty("providerOptions")
|
||||
})
|
||||
|
||||
it("preserves anthropic.signature while stripping anthropic.cacheControl", () => {
|
||||
const messages: RooMessage[] = [
|
||||
{
|
||||
role: "assistant" as const,
|
||||
content: [{ type: "text" as const, text: "hello" }],
|
||||
providerOptions: {
|
||||
anthropic: { cacheControl: { type: "ephemeral" }, signature: "abc123" },
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const result = stripCacheProviderOptions(messages)
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
role: "assistant" as const,
|
||||
content: [{ type: "text" as const, text: "hello" }],
|
||||
providerOptions: {
|
||||
anthropic: { signature: "abc123" },
|
||||
},
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it("preserves openrouter.reasoning_details unchanged", () => {
|
||||
const reasoningDetails = [{ type: "thinking", thinking: "hmm" }]
|
||||
const messages: RooMessage[] = [
|
||||
{
|
||||
role: "assistant" as const,
|
||||
content: [{ type: "text" as const, text: "hello" }],
|
||||
providerOptions: {
|
||||
openrouter: { reasoning_details: reasoningDetails },
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const result = stripCacheProviderOptions(messages)
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
role: "assistant" as const,
|
||||
content: [{ type: "text" as const, text: "hello" }],
|
||||
providerOptions: {
|
||||
openrouter: { reasoning_details: reasoningDetails },
|
||||
},
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it("strips cache keys while preserving non-cache keys across namespaces", () => {
|
||||
const reasoningDetails = [{ type: "thinking", thinking: "hmm" }]
|
||||
const messages: RooMessage[] = [
|
||||
{
|
||||
role: "assistant" as const,
|
||||
content: [{ type: "text" as const, text: "hello" }],
|
||||
providerOptions: {
|
||||
anthropic: { cacheControl: { type: "ephemeral" }, signature: "abc123" },
|
||||
bedrock: { cachePoint: { type: "default" } },
|
||||
openrouter: { reasoning_details: reasoningDetails },
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const result = stripCacheProviderOptions(messages)
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
role: "assistant" as const,
|
||||
content: [{ type: "text" as const, text: "hello" }],
|
||||
providerOptions: {
|
||||
anthropic: { signature: "abc123" },
|
||||
openrouter: { reasoning_details: reasoningDetails },
|
||||
},
|
||||
},
|
||||
])
|
||||
// bedrock namespace should be fully removed
|
||||
const resultOptions = (result[0] as unknown as Record<string, unknown>).providerOptions as Record<string, unknown>
|
||||
expect(resultOptions).not.toHaveProperty("bedrock")
|
||||
})
|
||||
|
||||
it("does not mutate the original array", () => {
|
||||
const original: RooMessage[] = [
|
||||
{
|
||||
role: "assistant" as const,
|
||||
content: [{ type: "text" as const, text: "hello" }],
|
||||
providerOptions: {
|
||||
anthropic: { cacheControl: { type: "ephemeral" }, signature: "abc123" },
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
// Snapshot original state before calling
|
||||
const originalSnapshot = JSON.parse(JSON.stringify(original))
|
||||
|
||||
stripCacheProviderOptions(original)
|
||||
|
||||
expect(original).toEqual(originalSnapshot)
|
||||
// Verify the providerOptions still has cacheControl on the original
|
||||
const originalOptions = (original[0] as unknown as Record<string, unknown>).providerOptions as Record<
|
||||
string,
|
||||
Record<string, unknown>
|
||||
>
|
||||
expect(originalOptions["anthropic"]["cacheControl"]).toEqual({ type: "ephemeral" })
|
||||
})
|
||||
|
||||
it("returns empty array for empty input", () => {
|
||||
const result = stripCacheProviderOptions([])
|
||||
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -12,7 +12,10 @@ import type { RooMessage, RooMessageHistory } from "./rooMessage"
|
|||
import { ROO_MESSAGE_VERSION } from "./rooMessage"
|
||||
import { convertAnthropicToRooMessages } from "./converters/anthropicToRoo"
|
||||
|
||||
export type ApiMessage = Anthropic.MessageParam & {
|
||||
/**
|
||||
* @deprecated This is the legacy Anthropic message format. Use {@link RooMessage} for the current format.
|
||||
*/
|
||||
export type LegacyApiMessage = Anthropic.MessageParam & {
|
||||
ts?: number
|
||||
isSummary?: boolean
|
||||
id?: string
|
||||
|
|
@ -40,13 +43,16 @@ export type ApiMessage = Anthropic.MessageParam & {
|
|||
isTruncationMarker?: boolean
|
||||
}
|
||||
|
||||
/** @deprecated Use {@link LegacyApiMessage} directly. This alias exists for backward compatibility only. */
|
||||
export type ApiMessage = LegacyApiMessage
|
||||
|
||||
export async function readApiMessages({
|
||||
taskId,
|
||||
globalStoragePath,
|
||||
}: {
|
||||
taskId: string
|
||||
globalStoragePath: string
|
||||
}): Promise<ApiMessage[]> {
|
||||
}): Promise<LegacyApiMessage[]> {
|
||||
const taskDir = await getTaskDirectoryPath(globalStoragePath, taskId)
|
||||
const filePath = path.join(taskDir, GlobalFileNames.apiConversationHistory)
|
||||
|
||||
|
|
@ -114,7 +120,7 @@ export async function saveApiMessages({
|
|||
taskId,
|
||||
globalStoragePath,
|
||||
}: {
|
||||
messages: ApiMessage[]
|
||||
messages: LegacyApiMessage[]
|
||||
taskId: string
|
||||
globalStoragePath: string
|
||||
}) {
|
||||
|
|
@ -194,7 +200,7 @@ export async function readRooMessages({
|
|||
return []
|
||||
}
|
||||
|
||||
return convertAnthropicToRooMessages(parsedData as ApiMessage[])
|
||||
return convertAnthropicToRooMessages(parsedData as LegacyApiMessage[])
|
||||
}
|
||||
|
||||
const primaryResult = await tryParseFile(filePath)
|
||||
|
|
@ -214,6 +220,48 @@ export async function readRooMessages({
|
|||
return []
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip transient cache-control provider options that are applied at request
|
||||
* time by applyCacheBreakpoints() and should not be persisted.
|
||||
*
|
||||
* Removes:
|
||||
* - anthropic.cacheControl
|
||||
* - bedrock.cachePoint
|
||||
*
|
||||
* Preserves all other providerOptions (e.g. anthropic.signature, openrouter.reasoning_details).
|
||||
*/
|
||||
export function stripCacheProviderOptions(messages: RooMessage[]): RooMessage[] {
|
||||
const cloned = structuredClone(messages)
|
||||
|
||||
for (const msg of cloned) {
|
||||
if (!("providerOptions" in msg) || (msg as { providerOptions?: unknown }).providerOptions == null) {
|
||||
continue
|
||||
}
|
||||
|
||||
const providerOptions = (msg as { providerOptions: Record<string, Record<string, unknown>> }).providerOptions
|
||||
|
||||
if (providerOptions["anthropic"] != null) {
|
||||
delete providerOptions["anthropic"]["cacheControl"]
|
||||
if (Object.keys(providerOptions["anthropic"]).length === 0) {
|
||||
delete providerOptions["anthropic"]
|
||||
}
|
||||
}
|
||||
|
||||
if (providerOptions["bedrock"] != null) {
|
||||
delete providerOptions["bedrock"]["cachePoint"]
|
||||
if (Object.keys(providerOptions["bedrock"]).length === 0) {
|
||||
delete providerOptions["bedrock"]
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(providerOptions).length === 0) {
|
||||
delete (msg as { providerOptions?: unknown }).providerOptions
|
||||
}
|
||||
}
|
||||
|
||||
return cloned
|
||||
}
|
||||
|
||||
/**
|
||||
* Save `RooMessage[]` wrapped in the versioned `RooMessageHistory` envelope.
|
||||
*
|
||||
|
|
@ -234,9 +282,10 @@ export async function saveRooMessages({
|
|||
try {
|
||||
const taskDir = await getTaskDirectoryPath(globalStoragePath, taskId)
|
||||
const filePath = path.join(taskDir, GlobalFileNames.apiConversationHistory)
|
||||
const strippedMessages = stripCacheProviderOptions(messages)
|
||||
const envelope: RooMessageHistory = {
|
||||
version: ROO_MESSAGE_VERSION,
|
||||
messages,
|
||||
messages: strippedMessages,
|
||||
}
|
||||
await safeWriteJson(filePath, envelope)
|
||||
return true
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { ApiMessage } from "../../apiMessages"
|
||||
import type { LegacyApiMessage } from "../../apiMessages"
|
||||
import type {
|
||||
RooUserMessage,
|
||||
RooAssistantMessage,
|
||||
|
|
@ -16,9 +16,9 @@ import { convertAnthropicToRooMessages } from "../anthropicToRoo"
|
|||
// Helpers
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/** Shorthand to create an ApiMessage with required fields. */
|
||||
function apiMsg(overrides: Partial<ApiMessage> & Pick<ApiMessage, "role" | "content">): ApiMessage {
|
||||
return overrides as ApiMessage
|
||||
/** Shorthand to create a LegacyApiMessage with required fields. */
|
||||
function apiMsg(overrides: Partial<LegacyApiMessage> & Pick<LegacyApiMessage, "role" | "content">): LegacyApiMessage {
|
||||
return overrides as LegacyApiMessage
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
|
@ -139,7 +139,7 @@ describe("user messages with URL image content", () => {
|
|||
|
||||
describe("user messages with tool_result blocks", () => {
|
||||
test("splits tool_result into RooToolMessage before RooUserMessage", () => {
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: LegacyApiMessage[] = [
|
||||
apiMsg({
|
||||
role: "assistant",
|
||||
content: [{ type: "tool_use", id: "call_1", name: "read_file", input: { path: "foo.ts" } }],
|
||||
|
|
@ -179,7 +179,7 @@ describe("user messages with tool_result blocks", () => {
|
|||
})
|
||||
|
||||
test("handles tool_result with array content (joins text with newlines)", () => {
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: LegacyApiMessage[] = [
|
||||
apiMsg({
|
||||
role: "assistant",
|
||||
content: [{ type: "tool_use", id: "call_2", name: "list_files", input: {} }],
|
||||
|
|
@ -204,7 +204,7 @@ describe("user messages with tool_result blocks", () => {
|
|||
})
|
||||
|
||||
test("handles tool_result with undefined content → (empty)", () => {
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: LegacyApiMessage[] = [
|
||||
apiMsg({
|
||||
role: "assistant",
|
||||
content: [{ type: "tool_use", id: "call_3", name: "run_command", input: {} }],
|
||||
|
|
@ -220,7 +220,7 @@ describe("user messages with tool_result blocks", () => {
|
|||
})
|
||||
|
||||
test("handles tool_result with empty string content → (empty)", () => {
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: LegacyApiMessage[] = [
|
||||
apiMsg({
|
||||
role: "assistant",
|
||||
content: [{ type: "tool_use", id: "call_4", name: "run_command", input: {} }],
|
||||
|
|
@ -242,7 +242,7 @@ describe("user messages with tool_result blocks", () => {
|
|||
|
||||
describe("user messages with mixed tool_result and text", () => {
|
||||
test("separates tool results from text/image parts correctly", () => {
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: LegacyApiMessage[] = [
|
||||
apiMsg({
|
||||
role: "assistant",
|
||||
content: [
|
||||
|
|
@ -287,7 +287,7 @@ describe("user messages with mixed tool_result and text", () => {
|
|||
})
|
||||
|
||||
test("only emits tool message when no text/image parts exist", () => {
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: LegacyApiMessage[] = [
|
||||
apiMsg({
|
||||
role: "assistant",
|
||||
content: [{ type: "tool_use", id: "tc_only", name: "some_tool", input: {} }],
|
||||
|
|
@ -733,7 +733,7 @@ describe("metadata preservation", () => {
|
|||
})
|
||||
|
||||
test("carries over metadata on tool messages (split from user)", () => {
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: LegacyApiMessage[] = [
|
||||
apiMsg({
|
||||
role: "assistant",
|
||||
content: [{ type: "tool_use", id: "tc_meta", name: "my_tool", input: {} }],
|
||||
|
|
@ -787,7 +787,7 @@ describe("metadata preservation", () => {
|
|||
|
||||
describe("tool name resolution", () => {
|
||||
test("resolves tool names from preceding assistant messages", () => {
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: LegacyApiMessage[] = [
|
||||
apiMsg({
|
||||
role: "assistant",
|
||||
content: [{ type: "tool_use", id: "tc_x", name: "execute_command", input: { command: "ls" } }],
|
||||
|
|
@ -803,7 +803,7 @@ describe("tool name resolution", () => {
|
|||
})
|
||||
|
||||
test("falls back to unknown_tool when tool call ID is not found", () => {
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: LegacyApiMessage[] = [
|
||||
apiMsg({
|
||||
role: "user",
|
||||
content: [{ type: "tool_result", tool_use_id: "nonexistent_id", content: "result" }],
|
||||
|
|
@ -815,7 +815,7 @@ describe("tool name resolution", () => {
|
|||
})
|
||||
|
||||
test("resolves tool names across multiple assistant messages", () => {
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: LegacyApiMessage[] = [
|
||||
apiMsg({
|
||||
role: "assistant",
|
||||
content: [{ type: "tool_use", id: "tc_first", name: "tool_alpha", input: {} }],
|
||||
|
|
@ -878,7 +878,7 @@ describe("empty/undefined content edge cases", () => {
|
|||
})
|
||||
|
||||
test("handles tool_result with image content blocks", () => {
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: LegacyApiMessage[] = [
|
||||
apiMsg({
|
||||
role: "assistant",
|
||||
content: [{ type: "tool_use", id: "tc_img", name: "screenshot", input: {} }],
|
||||
|
|
@ -914,7 +914,7 @@ describe("empty/undefined content edge cases", () => {
|
|||
|
||||
describe("full conversation round-trip", () => {
|
||||
test("converts a realistic multi-turn conversation", () => {
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: LegacyApiMessage[] = [
|
||||
// Turn 1: user asks a question
|
||||
apiMsg({ role: "user", content: "Can you read my config file?", ts: 1000 }),
|
||||
// Turn 2: assistant uses a tool
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* Converter from Anthropic-format `ApiMessage` to the new `RooMessage` format.
|
||||
* Converter from Anthropic-format `LegacyApiMessage` to the new `RooMessage` format.
|
||||
*
|
||||
* This is the critical backward-compatibility piece that allows old conversation
|
||||
* histories stored in Anthropic format to be read and converted to the new format.
|
||||
|
|
@ -9,7 +9,7 @@
|
|||
*/
|
||||
|
||||
import type { TextPart, ImagePart, ToolCallPart, ToolResultPart, ReasoningPart } from "../rooMessage"
|
||||
import type { ApiMessage } from "../apiMessages"
|
||||
import type { LegacyApiMessage } from "../apiMessages"
|
||||
import type {
|
||||
RooMessage,
|
||||
RooUserMessage,
|
||||
|
|
@ -28,10 +28,10 @@ import type {
|
|||
type LooseProviderOptions = Record<string, Record<string, unknown>>
|
||||
|
||||
/**
|
||||
* Extract Roo-specific metadata fields from an ApiMessage.
|
||||
* Extract Roo-specific metadata fields from a LegacyApiMessage.
|
||||
* Only includes fields that are actually defined (avoids `undefined` keys).
|
||||
*/
|
||||
function extractMetadata(message: ApiMessage): RooMessageMetadata {
|
||||
function extractMetadata(message: LegacyApiMessage): RooMessageMetadata {
|
||||
const metadata: RooMessageMetadata = {}
|
||||
if (message.ts !== undefined) metadata.ts = message.ts
|
||||
if (message.condenseId !== undefined) metadata.condenseId = message.condenseId
|
||||
|
|
@ -82,7 +82,7 @@ function attachReasoningDetails(
|
|||
}
|
||||
|
||||
/**
|
||||
* Convert an array of Anthropic-format `ApiMessage` objects to `RooMessage` format.
|
||||
* Convert an array of Anthropic-format `LegacyApiMessage` objects to `RooMessage` format.
|
||||
*
|
||||
* Conversion rules:
|
||||
* - User string content → `RooUserMessage` with `content: string`
|
||||
|
|
@ -93,10 +93,10 @@ function attachReasoningDetails(
|
|||
* - Standalone reasoning messages → `RooReasoningMessage`
|
||||
* - Metadata fields (ts, condenseId, etc.) are preserved on all output messages
|
||||
*
|
||||
* @param messages - Array of ApiMessage (Anthropic format with metadata)
|
||||
* @param messages - Array of LegacyApiMessage (Anthropic format with metadata)
|
||||
* @returns Array of RooMessage objects
|
||||
*/
|
||||
export function convertAnthropicToRooMessages(messages: ApiMessage[]): RooMessage[] {
|
||||
export function convertAnthropicToRooMessages(messages: LegacyApiMessage[]): RooMessage[] {
|
||||
const result: RooMessage[] = []
|
||||
|
||||
// First pass: build a map of tool call IDs to tool names from assistant messages.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
export { type ApiMessage, readApiMessages, saveApiMessages } from "./apiMessages"
|
||||
export { detectFormat, readRooMessages, saveRooMessages } from "./apiMessages"
|
||||
export { type LegacyApiMessage, type ApiMessage, readApiMessages, saveApiMessages } from "./apiMessages"
|
||||
export { detectFormat, readRooMessages, saveRooMessages, stripCacheProviderOptions } from "./apiMessages"
|
||||
export { readTaskMessages, saveTaskMessages } from "./taskMessages"
|
||||
export { taskMetadata } from "./taskMetadata"
|
||||
export type { RooMessage, RooMessageHistory, RooMessageMetadata } from "./rooMessage"
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ import { manageContext, willManageContext } from "../context-management"
|
|||
import { ClineProvider } from "../webview/ClineProvider"
|
||||
import { MultiSearchReplaceDiffStrategy } from "../diff/strategies/multi-search-replace"
|
||||
import {
|
||||
type ApiMessage,
|
||||
type LegacyApiMessage,
|
||||
readApiMessages,
|
||||
saveApiMessages,
|
||||
readTaskMessages,
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ vi.mock("../checkpointRestoreHandler", () => ({
|
|||
import { webviewMessageHandler } from "../webviewMessageHandler"
|
||||
import type { ClineProvider } from "../ClineProvider"
|
||||
import type { ClineMessage } from "@roo-code/types"
|
||||
import type { ApiMessage } from "../../task-persistence/apiMessages"
|
||||
import type { LegacyApiMessage } from "../../task-persistence/apiMessages"
|
||||
import { MessageManager } from "../../message-manager"
|
||||
|
||||
describe("webviewMessageHandler - Edit Message with Timestamp Fallback", () => {
|
||||
|
|
@ -54,7 +54,7 @@ describe("webviewMessageHandler - Edit Message with Timestamp Fallback", () => {
|
|||
mockCurrentTask = {
|
||||
taskId: "test-task-id",
|
||||
clineMessages: [] as ClineMessage[],
|
||||
apiConversationHistory: [] as ApiMessage[],
|
||||
apiConversationHistory: [] as LegacyApiMessage[],
|
||||
overwriteClineMessages: vi.fn(),
|
||||
overwriteApiConversationHistory: vi.fn(),
|
||||
handleWebviewAskResponse: vi.fn(),
|
||||
|
|
@ -126,7 +126,7 @@ describe("webviewMessageHandler - Edit Message with Timestamp Fallback", () => {
|
|||
},
|
||||
],
|
||||
},
|
||||
] as ApiMessage[]
|
||||
] as LegacyApiMessage[]
|
||||
|
||||
// Trigger edit confirmation
|
||||
await webviewMessageHandler(mockClineProvider, {
|
||||
|
|
@ -184,7 +184,7 @@ describe("webviewMessageHandler - Edit Message with Timestamp Fallback", () => {
|
|||
role: "assistant",
|
||||
content: [{ type: "text", text: "Response" }],
|
||||
},
|
||||
] as ApiMessage[]
|
||||
] as LegacyApiMessage[]
|
||||
|
||||
await webviewMessageHandler(mockClineProvider, {
|
||||
type: "editMessageConfirm",
|
||||
|
|
@ -244,7 +244,7 @@ describe("webviewMessageHandler - Edit Message with Timestamp Fallback", () => {
|
|||
role: "assistant",
|
||||
content: [{ type: "text", text: "Response" }],
|
||||
},
|
||||
] as ApiMessage[]
|
||||
] as LegacyApiMessage[]
|
||||
|
||||
await webviewMessageHandler(mockClineProvider, {
|
||||
type: "editMessageConfirm",
|
||||
|
|
@ -282,7 +282,7 @@ describe("webviewMessageHandler - Edit Message with Timestamp Fallback", () => {
|
|||
role: "assistant",
|
||||
content: [{ type: "text", text: "Old message 2" }],
|
||||
},
|
||||
] as ApiMessage[]
|
||||
] as LegacyApiMessage[]
|
||||
|
||||
await webviewMessageHandler(mockClineProvider, {
|
||||
type: "editMessageConfirm",
|
||||
|
|
@ -378,7 +378,7 @@ describe("webviewMessageHandler - Edit Message with Timestamp Fallback", () => {
|
|||
},
|
||||
],
|
||||
},
|
||||
] as ApiMessage[]
|
||||
] as LegacyApiMessage[]
|
||||
|
||||
// Edit the first user message
|
||||
await webviewMessageHandler(mockClineProvider, {
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import { customToolRegistry } from "@roo-code/core"
|
|||
import { CloudService } from "@roo-code/cloud"
|
||||
import { TelemetryService } from "@roo-code/telemetry"
|
||||
|
||||
import { type ApiMessage } from "../task-persistence/apiMessages"
|
||||
import { type LegacyApiMessage } from "../task-persistence/apiMessages"
|
||||
import { saveTaskMessages } from "../task-persistence"
|
||||
|
||||
import { ClineProvider } from "./ClineProvider"
|
||||
|
|
@ -131,11 +131,11 @@ export const webviewMessageHandler = async (
|
|||
|
||||
// Find all matching API messages by timestamp
|
||||
const allApiMatches = currentCline.apiConversationHistory
|
||||
.map((msg: ApiMessage, idx: number) => ({ msg, idx }))
|
||||
.filter(({ msg }: { msg: ApiMessage }) => msg.ts === messageTs)
|
||||
.map((msg: LegacyApiMessage, idx: number) => ({ msg, idx }))
|
||||
.filter(({ msg }: { msg: LegacyApiMessage }) => msg.ts === messageTs)
|
||||
|
||||
// Prefer non-summary message if multiple matches exist (handles timestamp collision after condense)
|
||||
const preferred = allApiMatches.find(({ msg }: { msg: ApiMessage }) => !msg.isSummary) || allApiMatches[0]
|
||||
const preferred = allApiMatches.find(({ msg }: { msg: LegacyApiMessage }) => !msg.isSummary) || allApiMatches[0]
|
||||
const apiConversationHistoryIndex = preferred?.idx ?? -1
|
||||
|
||||
return { messageIndex, apiConversationHistoryIndex }
|
||||
|
|
@ -148,7 +148,7 @@ export const webviewMessageHandler = async (
|
|||
const findFirstApiIndexAtOrAfter = (ts: number, currentCline: any) => {
|
||||
if (typeof ts !== "number") return -1
|
||||
return currentCline.apiConversationHistory.findIndex(
|
||||
(msg: ApiMessage) => typeof msg?.ts === "number" && (msg.ts as number) >= ts,
|
||||
(msg: LegacyApiMessage) => typeof msg?.ts === "number" && (msg.ts as number) >= ts,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue