Compare commits

...

19 Commits

Author SHA1 Message Date
Evan 02207ca5d9 Cline.ts - Factor out saveClineMessages [8/8] (#2365)
* refactor saveClineMessages

* changeset
2025-03-24 16:43:02 -07:00
celestial-vault 0c57d2e8d0 changeset 2025-03-20 14:55:26 -07:00
celestial-vault f4a0304bd6 refactor overwriteClineMessages 2025-03-20 14:55:08 -07:00
celestial-vault 765843af84 changeset 2025-03-20 14:46:26 -07:00
celestial-vault f8e3b62a17 refactor addToClineMessages 2025-03-20 14:45:58 -07:00
celestial-vault 64fed2c501 changeset 2025-03-20 14:24:43 -07:00
celestial-vault 636c2896bf refactor overwriteApiConversationHistory 2025-03-20 14:24:23 -07:00
celestial-vault e3ca261db5 changeset 2025-03-20 14:07:06 -07:00
celestial-vault e66f77a016 refactor addToApiConversationHistory 2025-03-20 14:06:45 -07:00
celestial-vault b3c71b0bd0 changeset 2025-03-20 13:56:02 -07:00
celestial-vault 4a93058f34 factor out getSavedClineMessages 2025-03-20 13:55:43 -07:00
celestial-vault 754ee766d1 delete extra changeset 2025-03-20 13:50:57 -07:00
celestial-vault d2e393e603 changeset 2025-03-20 13:48:45 -07:00
celestial-vault b0baefc3ba refactor out getSavedApiConversationHistory 2025-03-20 13:48:20 -07:00
celestial-vault 27571b4617 changeset 2025-03-20 13:41:26 -07:00
celestial-vault bf9164e1ae factor out saveApiConversationHistory 2025-03-20 13:40:52 -07:00
celestial-vault eea2c97835 changeset 2025-03-20 13:27:25 -07:00
celestial-vault cc67a77759 merge conflicts 2025-03-20 13:25:44 -07:00
celestial-vault 3d33834ce5 refactor out ensureTaskDirectoryExists 2025-03-20 13:21:58 -07:00
11 changed files with 447 additions and 146 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
refactor
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
refactor
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
refactor
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
refactor
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
refactor
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
refactor
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
refactor
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
refactor
+335 -145
View File
@@ -44,7 +44,7 @@ import {
ClineSayTool,
COMPLETION_RESULT_CHANGES_FLAG,
} from "../shared/ExtensionMessage"
import { getApiMetrics } from "../shared/getApiMetrics"
import { ApiMetrics, getApiMetrics } from "../shared/getApiMetrics"
import { HistoryItem } from "../shared/HistoryItem"
import { ClineAskResponse, ClineCheckpointRestore } from "../shared/WebviewMessage"
import { calculateApiCostAnthropic } from "../utils/cost"
@@ -66,6 +66,13 @@ import { DEFAULT_LANGUAGE_SETTINGS, getLanguageKey, LanguageDisplay, LanguageKey
import { telemetryService } from "../services/telemetry/TelemetryService"
import pTimeout from "p-timeout"
import { GlobalFileNames } from "../global-constants"
import {
ensureTaskDirectoryExists,
getSavedApiConversationHistory,
getSavedClineMessages,
saveApiConversationHistory,
saveClineMessages,
} from "./messages-io"
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) ?? path.join(os.homedir(), "Desktop") // may or may not exist but fs checking existence would immediately ask for permission which would be bad UX, need to come up with a better solution
@@ -178,108 +185,77 @@ export class Cline {
// Storing task to disk for history
private async ensureTaskDirectoryExists(): Promise<string> {
const globalStoragePath = this.providerRef.deref()?.context.globalStorageUri.fsPath
if (!globalStoragePath) {
throw new Error("Global storage uri is invalid")
private extractTaskMetricsAndMessages(clineMessages: ClineMessage[]) {
const apiMetrics = getApiMetrics(combineApiRequests(combineCommandSequences(clineMessages.slice(1))))
const taskMessage = clineMessages[0] // first message is always the task say
const lastRelevantMessage =
clineMessages[findLastIndex(clineMessages, (m) => !(m.ask === "resume_task" || m.ask === "resume_completed_task"))]
return {
apiMetrics,
taskMessage,
lastRelevantMessage,
}
const taskDir = path.join(globalStoragePath, "tasks", this.taskId)
await fs.mkdir(taskDir, { recursive: true })
return taskDir
}
private async getSavedApiConversationHistory(): Promise<Anthropic.MessageParam[]> {
const filePath = path.join(await this.ensureTaskDirectoryExists(), GlobalFileNames.apiConversationHistory)
const fileExists = await fileExistsAtPath(filePath)
if (fileExists) {
return JSON.parse(await fs.readFile(filePath, "utf8"))
}
return []
}
private async addToApiConversationHistory(message: Anthropic.MessageParam) {
this.apiConversationHistory.push(message)
await this.saveApiConversationHistory()
}
private async overwriteApiConversationHistory(newHistory: Anthropic.MessageParam[]) {
this.apiConversationHistory = newHistory
await this.saveApiConversationHistory()
}
private async saveApiConversationHistory() {
private async getTaskDirectorySize(taskDir: string): Promise<number> {
try {
const filePath = path.join(await this.ensureTaskDirectoryExists(), GlobalFileNames.apiConversationHistory)
await fs.writeFile(filePath, JSON.stringify(this.apiConversationHistory))
// getFolderSize.loose silently ignores errors
// returns # of bytes, size/1000/1000 = MB
return await getFolderSize.loose(taskDir)
} catch (error) {
// in the off chance this fails, we don't want to stop the task
console.error("Failed to save API conversation history:", error)
console.error("Failed to get task directory size:", taskDir, error)
return 0
}
}
private async getSavedClineMessages(): Promise<ClineMessage[]> {
const filePath = path.join(await this.ensureTaskDirectoryExists(), GlobalFileNames.uiMessages)
if (await fileExistsAtPath(filePath)) {
return JSON.parse(await fs.readFile(filePath, "utf8"))
} else {
// check old location
const oldPath = path.join(await this.ensureTaskDirectoryExists(), "claude_messages.json")
if (await fileExistsAtPath(oldPath)) {
const data = JSON.parse(await fs.readFile(oldPath, "utf8"))
await fs.unlink(oldPath) // remove old file
return data
}
}
return []
private async updateTaskHistoryData(
providerRef: WeakRef<ClineProvider>,
taskId: string,
lastRelevantMessage: ClineMessage,
taskMessage: ClineMessage,
apiMetrics: ApiMetrics,
taskDirSize: number,
checkpointTracker: CheckpointTracker | undefined,
conversationHistoryDeletedRange: [number, number] | undefined,
) {
await providerRef.deref()?.updateTaskHistory({
id: taskId,
ts: lastRelevantMessage.ts,
task: taskMessage.text ?? "",
tokensIn: apiMetrics.totalTokensIn,
tokensOut: apiMetrics.totalTokensOut,
cacheWrites: apiMetrics.totalCacheWrites,
cacheReads: apiMetrics.totalCacheReads,
totalCost: apiMetrics.totalCost,
size: taskDirSize,
shadowGitConfigWorkTree: await checkpointTracker?.getShadowGitConfigWorkTree(),
conversationHistoryDeletedRange: conversationHistoryDeletedRange,
})
}
private async addToClineMessages(message: ClineMessage) {
// these values allow us to reconstruct the conversation history at the time this cline message was created
// it's important that apiConversationHistory is initialized before we add cline messages
message.conversationHistoryIndex = this.apiConversationHistory.length - 1 // NOTE: this is the index of the last added message which is the user message, and once the clinemessages have been presented we update the apiconversationhistory with the completed assistant message. This means when resetting to a message, we need to +1 this index to get the correct assistant message that this tool use corresponds to
message.conversationHistoryDeletedRange = this.conversationHistoryDeletedRange
this.clineMessages.push(message)
await this.saveClineMessages()
}
private async overwriteClineMessages(newMessages: ClineMessage[]) {
this.clineMessages = newMessages
await this.saveClineMessages()
}
private async saveClineMessages() {
private async processAndSaveClineMessages(
providerRef: WeakRef<ClineProvider>,
globalStoragePath: string | undefined,
taskId: string,
clineMessages: ClineMessage[],
checkpointTracker: CheckpointTracker | undefined,
conversationHistoryDeletedRange: [number, number] | undefined,
) {
try {
const taskDir = await this.ensureTaskDirectoryExists()
const filePath = path.join(taskDir, GlobalFileNames.uiMessages)
await fs.writeFile(filePath, JSON.stringify(this.clineMessages))
// combined as they are in ChatView
const apiMetrics = getApiMetrics(combineApiRequests(combineCommandSequences(this.clineMessages.slice(1))))
const taskMessage = this.clineMessages[0] // first message is always the task say
const lastRelevantMessage =
this.clineMessages[
findLastIndex(this.clineMessages, (m) => !(m.ask === "resume_task" || m.ask === "resume_completed_task"))
]
let taskDirSize = 0
try {
// getFolderSize.loose silently ignores errors
// returns # of bytes, size/1000/1000 = MB
taskDirSize = await getFolderSize.loose(taskDir)
} catch (error) {
console.error("Failed to get task directory size:", taskDir, error)
}
await this.providerRef.deref()?.updateTaskHistory({
id: this.taskId,
ts: lastRelevantMessage.ts,
task: taskMessage.text ?? "",
tokensIn: apiMetrics.totalTokensIn,
tokensOut: apiMetrics.totalTokensOut,
cacheWrites: apiMetrics.totalCacheWrites,
cacheReads: apiMetrics.totalCacheReads,
totalCost: apiMetrics.totalCost,
size: taskDirSize,
shadowGitConfigWorkTree: await this.checkpointTracker?.getShadowGitConfigWorkTree(),
conversationHistoryDeletedRange: this.conversationHistoryDeletedRange,
})
const taskDir = await saveClineMessages(globalStoragePath, taskId, clineMessages)
const { apiMetrics, taskMessage, lastRelevantMessage } = this.extractTaskMetricsAndMessages(clineMessages)
const taskDirSize = await this.getTaskDirectorySize(taskDir)
await this.updateTaskHistoryData(
providerRef,
taskId,
lastRelevantMessage,
taskMessage,
apiMetrics,
taskDirSize,
checkpointTracker,
conversationHistoryDeletedRange,
)
} catch (error) {
console.error("Failed to save cline messages:", error)
}
@@ -336,14 +312,27 @@ export class Cline {
0,
(message.conversationHistoryIndex || 0) + 2,
) // +1 since this index corresponds to the last user message, and another +1 since slice end index is exclusive
await this.overwriteApiConversationHistory(newConversationHistory)
this.apiConversationHistory = newConversationHistory
const globalStoragePath = this.providerRef.deref()?.context.globalStorageUri.fsPath
const taskId = this.taskId
await saveApiConversationHistory(globalStoragePath, taskId, this.apiConversationHistory)
// aggregate deleted api reqs info so we don't lose costs/tokens
const deletedMessages = this.clineMessages.slice(messageIndex + 1)
const deletedApiReqsMetrics = getApiMetrics(combineApiRequests(combineCommandSequences(deletedMessages)))
const newClineMessages = this.clineMessages.slice(0, messageIndex + 1)
await this.overwriteClineMessages(newClineMessages) // calls saveClineMessages which saves historyItem
this.clineMessages = newClineMessages
await this.processAndSaveClineMessages(
this.providerRef,
this.providerRef.deref()?.context.globalStorageUri.fsPath,
this.taskId,
newClineMessages,
this.checkpointTracker,
this.conversationHistoryDeletedRange,
)
await this.say(
"deleted_api_reqs",
@@ -384,7 +373,14 @@ export class Cline {
})
}
await this.saveClineMessages()
await this.processAndSaveClineMessages(
this.providerRef,
this.providerRef.deref()?.context.globalStorageUri.fsPath,
this.taskId,
this.clineMessages,
this.checkpointTracker,
this.conversationHistoryDeletedRange,
)
await this.providerRef.deref()?.postMessageToWebview({ type: "relinquishControl" })
@@ -605,7 +601,7 @@ export class Cline {
lastMessage.text = text
lastMessage.partial = partial
// todo be more efficient about saving and posting only new data or one whole message at a time so ignore partial for saves, and only post parts of partial message instead of whole array in new listener
// await this.saveClineMessages()
// await this.processAndSaveClineMessages()
// await this.providerRef.deref()?.postStateToWebview()
await this.providerRef.deref()?.postMessageToWebview({
type: "partialMessage",
@@ -619,13 +615,28 @@ export class Cline {
// this.askResponseImages = undefined
askTs = Date.now()
this.lastMessageTs = askTs
await this.addToClineMessages({
// conversationHistoryIndex and conversationHistoryDeletedRange allow us to reconstruct the conversation history at the time this cline message was created
// it's important that apiConversationHistory is initialized before we add cline messages
const message: ClineMessage = {
ts: askTs,
type: "ask",
ask: type,
text,
partial,
})
conversationHistoryIndex: this.apiConversationHistory.length - 1, // NOTE: this is the index of the last added message which is the user message, and once the clinemessages have been presented we update the apiconversationhistory with the completed assistant message. This means when resetting to a message, we need to +1 this index to get the correct assistant message that this tool use corresponds to
conversationHistoryDeletedRange: this.conversationHistoryDeletedRange,
}
this.clineMessages.push(message)
await this.processAndSaveClineMessages(
this.providerRef,
this.providerRef.deref()?.context.globalStorageUri.fsPath,
this.taskId,
this.clineMessages,
this.checkpointTracker,
this.conversationHistoryDeletedRange,
)
await this.providerRef.deref()?.postStateToWebview()
throw new Error("Current ask promise was ignored 2")
}
@@ -648,7 +659,14 @@ export class Cline {
// lastMessage.ts = askTs
lastMessage.text = text
lastMessage.partial = false
await this.saveClineMessages()
await this.processAndSaveClineMessages(
this.providerRef,
this.providerRef.deref()?.context.globalStorageUri.fsPath,
this.taskId,
this.clineMessages,
this.checkpointTracker,
this.conversationHistoryDeletedRange,
)
// await this.providerRef.deref()?.postStateToWebview()
await this.providerRef.deref()?.postMessageToWebview({
type: "partialMessage",
@@ -661,12 +679,25 @@ export class Cline {
this.askResponseImages = undefined
askTs = Date.now()
this.lastMessageTs = askTs
await this.addToClineMessages({
const message: ClineMessage = {
ts: askTs,
type: "ask",
ask: type,
text,
})
conversationHistoryIndex: this.apiConversationHistory.length - 1,
conversationHistoryDeletedRange: this.conversationHistoryDeletedRange,
}
this.clineMessages.push(message)
await this.processAndSaveClineMessages(
this.providerRef,
this.providerRef.deref()?.context.globalStorageUri.fsPath,
this.taskId,
this.clineMessages,
this.checkpointTracker,
this.conversationHistoryDeletedRange,
)
await this.providerRef.deref()?.postStateToWebview()
}
}
@@ -678,12 +709,25 @@ export class Cline {
this.askResponseImages = undefined
askTs = Date.now()
this.lastMessageTs = askTs
await this.addToClineMessages({
const message: ClineMessage = {
ts: askTs,
type: "ask",
ask: type,
text,
})
conversationHistoryIndex: this.apiConversationHistory.length - 1,
conversationHistoryDeletedRange: this.conversationHistoryDeletedRange,
}
this.clineMessages.push(message)
await this.processAndSaveClineMessages(
this.providerRef,
this.providerRef.deref()?.context.globalStorageUri.fsPath,
this.taskId,
this.clineMessages,
this.checkpointTracker,
this.conversationHistoryDeletedRange,
)
await this.providerRef.deref()?.postStateToWebview()
}
@@ -731,14 +775,26 @@ export class Cline {
// this is a new partial message, so add it with partial state
const sayTs = Date.now()
this.lastMessageTs = sayTs
await this.addToClineMessages({
const message: ClineMessage = {
ts: sayTs,
type: "say",
say: type,
text,
images,
partial,
})
conversationHistoryIndex: this.apiConversationHistory.length - 1,
conversationHistoryDeletedRange: this.conversationHistoryDeletedRange,
}
this.clineMessages.push(message)
await this.processAndSaveClineMessages(
this.providerRef,
this.providerRef.deref()?.context.globalStorageUri.fsPath,
this.taskId,
this.clineMessages,
this.checkpointTracker,
this.conversationHistoryDeletedRange,
)
await this.providerRef.deref()?.postStateToWebview()
}
} else {
@@ -752,7 +808,14 @@ export class Cline {
lastMessage.partial = false
// instead of streaming partialMessage events, we do a save and post like normal to persist to disk
await this.saveClineMessages()
await this.processAndSaveClineMessages(
this.providerRef,
this.providerRef.deref()?.context.globalStorageUri.fsPath,
this.taskId,
this.clineMessages,
this.checkpointTracker,
this.conversationHistoryDeletedRange,
)
// await this.providerRef.deref()?.postStateToWebview()
await this.providerRef.deref()?.postMessageToWebview({
type: "partialMessage",
@@ -762,13 +825,26 @@ export class Cline {
// this is a new partial=false message, so add it like normal
const sayTs = Date.now()
this.lastMessageTs = sayTs
await this.addToClineMessages({
const message: ClineMessage = {
ts: sayTs,
type: "say",
say: type,
text,
images,
})
conversationHistoryIndex: this.apiConversationHistory.length - 1,
conversationHistoryDeletedRange: this.conversationHistoryDeletedRange,
}
this.clineMessages.push(message)
await this.processAndSaveClineMessages(
this.providerRef,
this.providerRef.deref()?.context.globalStorageUri.fsPath,
this.taskId,
this.clineMessages,
this.checkpointTracker,
this.conversationHistoryDeletedRange,
)
await this.providerRef.deref()?.postStateToWebview()
}
}
@@ -776,13 +852,27 @@ export class Cline {
// this is a new non-partial message, so add it like normal
const sayTs = Date.now()
this.lastMessageTs = sayTs
await this.addToClineMessages({
const message: ClineMessage = {
ts: sayTs,
type: "say",
say: type,
text,
images,
})
conversationHistoryIndex: this.apiConversationHistory.length - 1,
conversationHistoryDeletedRange: this.conversationHistoryDeletedRange,
}
this.clineMessages.push(message)
await this.processAndSaveClineMessages(
this.providerRef,
this.providerRef.deref()?.context.globalStorageUri.fsPath,
this.taskId,
this.clineMessages,
this.checkpointTracker,
this.conversationHistoryDeletedRange,
)
await this.providerRef.deref()?.postStateToWebview()
}
}
@@ -801,7 +891,14 @@ export class Cline {
const lastMessage = this.clineMessages.at(-1)
if (lastMessage?.partial && lastMessage.type === type && (lastMessage.ask === askOrSay || lastMessage.say === askOrSay)) {
this.clineMessages.pop()
await this.saveClineMessages()
await this.processAndSaveClineMessages(
this.providerRef,
this.providerRef.deref()?.context.globalStorageUri.fsPath,
this.taskId,
this.clineMessages,
this.checkpointTracker,
this.conversationHistoryDeletedRange,
)
await this.providerRef.deref()?.postStateToWebview()
}
}
@@ -840,8 +937,9 @@ export class Cline {
// if (!doesShadowGitExist) {
// this.checkpointTrackerErrorMessage = "Checkpoints are only available for new tasks"
// }
const modifiedClineMessages = await this.getSavedClineMessages()
const globalStoragePath = this.providerRef.deref()?.context.globalStorageUri.fsPath
const taskId = this.taskId
const modifiedClineMessages = await getSavedClineMessages(globalStoragePath, taskId)
// Remove any resume messages that may have been added before
const lastRelevantMessageIndex = findLastIndex(
@@ -865,12 +963,22 @@ export class Cline {
}
}
await this.overwriteClineMessages(modifiedClineMessages)
this.clineMessages = await this.getSavedClineMessages()
this.clineMessages = modifiedClineMessages
await this.processAndSaveClineMessages(
this.providerRef,
this.providerRef.deref()?.context.globalStorageUri.fsPath,
this.taskId,
this.clineMessages,
this.checkpointTracker,
this.conversationHistoryDeletedRange,
)
this.clineMessages = await getSavedClineMessages(globalStoragePath, taskId)
// Now present the cline messages to the user and ask if they want to resume (NOTE: we ran into a bug before where the apiconversationhistory wouldnt be initialized when opening a old task, and it was because we were waiting for resume)
// This is important in case the user deletes messages without resuming the task first
this.apiConversationHistory = await this.getSavedApiConversationHistory()
this.apiConversationHistory = await getSavedApiConversationHistory(globalStoragePath, taskId)
const lastClineMessage = this.clineMessages
.slice()
@@ -907,7 +1015,10 @@ export class Cline {
// need to make sure that the api conversation history can be resumed by the api, even if it goes out of sync with cline messages
const existingApiConversationHistory: Anthropic.Messages.MessageParam[] = await this.getSavedApiConversationHistory()
const existingApiConversationHistory: Anthropic.Messages.MessageParam[] = await getSavedApiConversationHistory(
globalStoragePath,
taskId,
)
// if the last message is an assistant message, we need to check if there's tool use since every tool use has to have a tool response
// if there's no tool use and only a text block, then we can just add a user message
@@ -1042,7 +1153,9 @@ export class Cline {
newUserContent.push(...formatResponse.imageBlocks(responseImages))
}
await this.overwriteApiConversationHistory(modifiedApiConversationHistory)
this.apiConversationHistory = modifiedApiConversationHistory
await saveApiConversationHistory(globalStoragePath, taskId, this.apiConversationHistory)
await this.initiateTaskLoop(newUserContent, false)
}
@@ -1103,7 +1216,14 @@ export class Cline {
const lastCheckpointMessage = findLast(this.clineMessages, (m) => m.say === "checkpoint_created")
if (lastCheckpointMessage) {
lastCheckpointMessage.lastCheckpointHash = commitHash
await this.saveClineMessages()
await this.processAndSaveClineMessages(
this.providerRef,
this.providerRef.deref()?.context.globalStorageUri.fsPath,
this.taskId,
this.clineMessages,
this.checkpointTracker,
this.conversationHistoryDeletedRange,
)
}
}) // silently fails for now
@@ -1118,7 +1238,14 @@ export class Cline {
)
if (lastCompletionResultMessage) {
lastCompletionResultMessage.lastCheckpointHash = commitHash
await this.saveClineMessages()
await this.processAndSaveClineMessages(
this.providerRef,
this.providerRef.deref()?.context.globalStorageUri.fsPath,
this.taskId,
this.clineMessages,
this.checkpointTracker,
this.conversationHistoryDeletedRange,
)
}
}
@@ -1155,7 +1282,7 @@ export class Cline {
// }
// }
// // Save the updated messages
// await this.saveClineMessages()
// await this.processAndSaveClineMessages()
// }
}
@@ -1388,8 +1515,14 @@ export class Cline {
this.conversationHistoryDeletedRange,
keep,
)
await this.saveClineMessages() // saves task history item which we use to keep track of conversation history deleted range
// await this.overwriteApiConversationHistory(truncatedMessages)
await this.processAndSaveClineMessages(
this.providerRef,
this.providerRef.deref()?.context.globalStorageUri.fsPath,
this.taskId,
this.clineMessages,
this.checkpointTracker,
this.conversationHistoryDeletedRange,
) // saves task history item which we use to keep track of conversation history deleted range
}
}
}
@@ -2760,7 +2893,14 @@ export class Cline {
...sharedMessage,
selected: text,
} satisfies ClineAskQuestion)
await this.saveClineMessages()
await this.processAndSaveClineMessages(
this.providerRef,
this.providerRef.deref()?.context.globalStorageUri.fsPath,
this.taskId,
this.clineMessages,
this.checkpointTracker,
this.conversationHistoryDeletedRange,
)
}
} else {
// Option not selected, send user feedback
@@ -2823,7 +2963,14 @@ export class Cline {
...sharedMessage,
selected: text,
} satisfies ClinePlanModeResponse)
await this.saveClineMessages()
await this.processAndSaveClineMessages(
this.providerRef,
this.providerRef.deref()?.context.globalStorageUri.fsPath,
this.taskId,
this.clineMessages,
this.checkpointTracker,
this.conversationHistoryDeletedRange,
)
}
} else {
// Option not selected, send user feedback
@@ -2892,7 +3039,14 @@ export class Cline {
) {
lastCompletionResultMessage.text += COMPLETION_RESULT_CHANGES_FLAG
}
await this.saveClineMessages()
await this.processAndSaveClineMessages(
this.providerRef,
this.providerRef.deref()?.context.globalStorageUri.fsPath,
this.taskId,
this.clineMessages,
this.checkpointTracker,
this.conversationHistoryDeletedRange,
)
}
try {
@@ -3139,7 +3293,7 @@ export class Cline {
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error"
console.error("Failed to initialize checkpoint tracker:", errorMessage)
this.checkpointTrackerErrorMessage = errorMessage // will be displayed right away since we saveClineMessages next which posts state to webview
this.checkpointTrackerErrorMessage = errorMessage // will be displayed right away since we processAndSaveClineMessages next which posts state to webview
}
}
@@ -3149,7 +3303,14 @@ export class Cline {
const lastCheckpointMessage = findLast(this.clineMessages, (m) => m.say === "checkpoint_created")
if (lastCheckpointMessage) {
lastCheckpointMessage.lastCheckpointHash = commitHash
await this.saveClineMessages()
await this.processAndSaveClineMessages(
this.providerRef,
this.providerRef.deref()?.context.globalStorageUri.fsPath,
this.taskId,
this.clineMessages,
this.checkpointTracker,
this.conversationHistoryDeletedRange,
)
}
}
@@ -3158,10 +3319,13 @@ export class Cline {
// add environment details as its own text block, separate from tool results
userContent.push({ type: "text", text: environmentDetails })
await this.addToApiConversationHistory({
this.apiConversationHistory.push({
role: "user",
content: userContent,
})
const globalStoragePath = this.providerRef.deref()?.context.globalStorageUri.fsPath
const taskId = this.taskId
await saveApiConversationHistory(globalStoragePath, taskId, this.apiConversationHistory)
telemetryService.captureConversationTurnEvent(this.taskId, this.apiProvider, this.api.getModel().id, "user")
@@ -3170,7 +3334,14 @@ export class Cline {
this.clineMessages[lastApiReqIndex].text = JSON.stringify({
request: userContent.map((block) => formatContentBlockToMarkdown(block)).join("\n\n"),
} satisfies ClineApiReqInfo)
await this.saveClineMessages()
await this.processAndSaveClineMessages(
this.providerRef,
this.providerRef.deref()?.context.globalStorageUri.fsPath,
this.taskId,
this.clineMessages,
this.checkpointTracker,
this.conversationHistoryDeletedRange,
)
await this.providerRef.deref()?.postStateToWebview()
try {
@@ -3216,11 +3387,12 @@ export class Cline {
lastMessage.partial = false
// instead of streaming partialMessage events, we do a save and post like normal to persist to disk
console.log("updating partial message", lastMessage)
// await this.saveClineMessages()
// await this.processAndSaveClineMessages()
}
// Let assistant know their response was interrupted for when task is resumed
await this.addToApiConversationHistory({
this.apiConversationHistory.push({
role: "assistant",
content: [
{
@@ -3235,11 +3407,18 @@ export class Cline {
},
],
})
await saveApiConversationHistory(globalStoragePath, taskId, this.apiConversationHistory)
// update api_req_started to have cancelled and cost, so that we can display the cost of the partial stream
updateApiReqMsg(cancelReason, streamingFailedMessage)
await this.saveClineMessages()
await this.processAndSaveClineMessages(
this.providerRef,
this.providerRef.deref()?.context.globalStorageUri.fsPath,
this.taskId,
this.clineMessages,
this.checkpointTracker,
this.conversationHistoryDeletedRange,
)
telemetryService.captureConversationTurnEvent(this.taskId, this.apiProvider, this.api.getModel().id, "assistant")
// signals to provider that it can retrieve the saved messages from disk, as abortTask can not be awaited on in nature
@@ -3353,8 +3532,14 @@ export class Cline {
totalCost = apiStreamUsage.totalCost
}
updateApiReqMsg()
await this.saveClineMessages()
await this.providerRef.deref()?.postStateToWebview()
await this.processAndSaveClineMessages(
this.providerRef,
this.providerRef.deref()?.context.globalStorageUri.fsPath,
this.taskId,
this.clineMessages,
this.checkpointTracker,
this.conversationHistoryDeletedRange,
)
})
}
@@ -3377,7 +3562,14 @@ export class Cline {
}
updateApiReqMsg()
await this.saveClineMessages()
await this.processAndSaveClineMessages(
this.providerRef,
this.providerRef.deref()?.context.globalStorageUri.fsPath,
this.taskId,
this.clineMessages,
this.checkpointTracker,
this.conversationHistoryDeletedRange,
)
await this.providerRef.deref()?.postStateToWebview()
// now add to apiconversationhistory
@@ -3386,10 +3578,11 @@ export class Cline {
if (assistantMessage.length > 0) {
telemetryService.captureConversationTurnEvent(this.taskId, this.apiProvider, this.api.getModel().id, "assistant")
await this.addToApiConversationHistory({
this.apiConversationHistory.push({
role: "assistant",
content: [{ type: "text", text: assistantMessage }],
})
await saveApiConversationHistory(globalStoragePath, taskId, this.apiConversationHistory)
// NOTE: this comment is here for future reference - this was a workaround for userMessageContent not getting set to true. It was due to it not recursively calling for partial blocks when didRejectTool, so it would get stuck waiting for a partial block to complete before it could continue.
// in case the content blocks finished
@@ -3421,15 +3614,12 @@ export class Cline {
"error",
"Unexpected API Response: The language model did not provide any assistant messages. This may indicate an issue with the API or the model's output.",
)
await this.addToApiConversationHistory({
this.apiConversationHistory.push({
role: "assistant",
content: [
{
type: "text",
text: "Failure: I did not provide a response.",
},
],
content: [{ type: "text", text: "Failure: I did not provide a response." }],
})
await saveApiConversationHistory(globalStoragePath, taskId, this.apiConversationHistory)
}
return didEndLoop // will always be false for now
+71
View File
@@ -0,0 +1,71 @@
import fs from "fs/promises"
import path from "path"
import { GlobalFileNames } from "../global-constants"
import Anthropic from "@anthropic-ai/sdk"
import { fileExistsAtPath } from "../utils/fs"
import { ClineMessage } from "../shared/ExtensionMessage"
export async function ensureTaskDirectoryExists(globalStoragePath: string | undefined, taskId: string): Promise<string> {
if (!globalStoragePath) {
throw new Error("Global storage uri is invalid")
}
const taskDir = path.join(globalStoragePath, "tasks", taskId)
await fs.mkdir(taskDir, { recursive: true })
return taskDir
}
export async function saveApiConversationHistory(
globalStoragePath: string | undefined,
taskId: string,
apiConversationHistory: Anthropic.MessageParam[],
) {
try {
const filePath = path.join(
await ensureTaskDirectoryExists(globalStoragePath, taskId),
GlobalFileNames.apiConversationHistory,
)
await fs.writeFile(filePath, JSON.stringify(apiConversationHistory))
} catch (error) {
// in the off chance this fails, we don't want to stop the task
console.error("Failed to save API conversation history:", error)
}
}
export async function getSavedApiConversationHistory(
globalStoragePath: string | undefined,
taskId: string,
): Promise<Anthropic.MessageParam[]> {
const filePath = path.join(await ensureTaskDirectoryExists(globalStoragePath, taskId), GlobalFileNames.apiConversationHistory)
const fileExists = await fileExistsAtPath(filePath)
if (fileExists) {
return JSON.parse(await fs.readFile(filePath, "utf8"))
}
return []
}
export async function getSavedClineMessages(globalStoragePath: string | undefined, taskId: string): Promise<ClineMessage[]> {
const filePath = path.join(await ensureTaskDirectoryExists(globalStoragePath, taskId), GlobalFileNames.uiMessages)
if (await fileExistsAtPath(filePath)) {
return JSON.parse(await fs.readFile(filePath, "utf8"))
} else {
// check old location
const oldPath = path.join(await ensureTaskDirectoryExists(globalStoragePath, taskId), "claude_messages.json")
if (await fileExistsAtPath(oldPath)) {
const data = JSON.parse(await fs.readFile(oldPath, "utf8"))
await fs.unlink(oldPath) // remove old file
return data
}
}
return []
}
export async function saveClineMessages(
globalStoragePath: string | undefined,
taskId: string,
clineMessages: ClineMessage[],
): Promise<string> {
const taskDir = await ensureTaskDirectoryExists(globalStoragePath, taskId)
const filePath = path.join(taskDir, GlobalFileNames.uiMessages)
await fs.writeFile(filePath, JSON.stringify(clineMessages))
return taskDir
}
+1 -1
View File
@@ -1,6 +1,6 @@
import { ClineMessage } from "./ExtensionMessage"
interface ApiMetrics {
export interface ApiMetrics {
totalTokensIn: number
totalTokensOut: number
totalCacheWrites?: number