|
|
@@ -1,4 +1,3 @@
|
|
|
-import fs from "fs/promises"
|
|
|
import * as path from "path"
|
|
|
import os from "os"
|
|
|
import crypto from "crypto"
|
|
|
@@ -8,7 +7,6 @@ import { Anthropic } from "@anthropic-ai/sdk"
|
|
|
import cloneDeep from "clone-deep"
|
|
|
import delay from "delay"
|
|
|
import pWaitFor from "p-wait-for"
|
|
|
-import getFolderSize from "get-folder-size"
|
|
|
import { serializeError } from "serialize-error"
|
|
|
import * as vscode from "vscode"
|
|
|
|
|
|
@@ -58,7 +56,6 @@ import { TerminalRegistry } from "../integrations/terminal/TerminalRegistry"
|
|
|
|
|
|
// utils
|
|
|
import { calculateApiCostAnthropic } from "../utils/cost"
|
|
|
-import { fileExistsAtPath } from "../utils/fs"
|
|
|
import { arePathsEqual, getWorkspacePath } from "../utils/path"
|
|
|
|
|
|
// tools
|
|
|
@@ -93,6 +90,7 @@ import { truncateConversationIfNeeded } from "./sliding-window"
|
|
|
import { ClineProvider } from "./webview/ClineProvider"
|
|
|
import { validateToolUse } from "./mode-validator"
|
|
|
import { MultiSearchReplaceDiffStrategy } from "./diff/strategies/multi-search-replace"
|
|
|
+import { readApiMessages, saveApiMessages, readTaskMessages, saveTaskMessages, taskMetadata } from "./task-persistence"
|
|
|
|
|
|
type UserContent = Array<Anthropic.Messages.ContentBlockParam>
|
|
|
|
|
|
@@ -171,6 +169,7 @@ export class Cline extends EventEmitter<ClineEvents> {
|
|
|
|
|
|
// Not private since it needs to be accessible by tools.
|
|
|
providerRef: WeakRef<ClineProvider>
|
|
|
+ private readonly globalStoragePath: string
|
|
|
private abort: boolean = false
|
|
|
didFinishAbortingStream = false
|
|
|
abandoned = false
|
|
|
@@ -244,6 +243,7 @@ export class Cline extends EventEmitter<ClineEvents> {
|
|
|
this.fuzzyMatchThreshold = fuzzyMatchThreshold
|
|
|
this.consecutiveMistakeLimit = consecutiveMistakeLimit
|
|
|
this.providerRef = new WeakRef(provider)
|
|
|
+ this.globalStoragePath = provider.context.globalStorageUri.fsPath
|
|
|
this.diffViewProvider = new DiffViewProvider(this.cwd)
|
|
|
this.enableCheckpoints = enableCheckpoints
|
|
|
|
|
|
@@ -294,26 +294,8 @@ export class Cline extends EventEmitter<ClineEvents> {
|
|
|
|
|
|
// 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")
|
|
|
- }
|
|
|
-
|
|
|
- // Use storagePathManager to retrieve the task storage directory
|
|
|
- const { getTaskDirectoryPath } = await import("../shared/storagePathManager")
|
|
|
- return getTaskDirectoryPath(globalStoragePath, this.taskId)
|
|
|
- }
|
|
|
-
|
|
|
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 []
|
|
|
+ return readApiMessages({ taskId: this.taskId, globalStoragePath: this.globalStoragePath })
|
|
|
}
|
|
|
|
|
|
private async addToApiConversationHistory(message: Anthropic.MessageParam) {
|
|
|
@@ -329,8 +311,11 @@ export class Cline extends EventEmitter<ClineEvents> {
|
|
|
|
|
|
private async saveApiConversationHistory() {
|
|
|
try {
|
|
|
- const filePath = path.join(await this.ensureTaskDirectoryExists(), GlobalFileNames.apiConversationHistory)
|
|
|
- await fs.writeFile(filePath, JSON.stringify(this.apiConversationHistory))
|
|
|
+ await saveApiMessages({
|
|
|
+ messages: this.apiConversationHistory,
|
|
|
+ taskId: this.taskId,
|
|
|
+ globalStoragePath: this.globalStoragePath,
|
|
|
+ })
|
|
|
} 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)
|
|
|
@@ -338,21 +323,7 @@ export class Cline extends EventEmitter<ClineEvents> {
|
|
|
}
|
|
|
|
|
|
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 []
|
|
|
+ return readTaskMessages({ taskId: this.taskId, globalStoragePath: this.globalStoragePath })
|
|
|
}
|
|
|
|
|
|
private async addToClineMessages(message: ClineMessage) {
|
|
|
@@ -374,54 +345,25 @@ export class Cline extends EventEmitter<ClineEvents> {
|
|
|
this.emit("message", { action: "updated", message: partialMessage })
|
|
|
}
|
|
|
|
|
|
- private taskDirSize = 0
|
|
|
- private taskDirSizeCheckedAt = 0
|
|
|
- private readonly taskDirSizeCheckInterval = 30_000
|
|
|
-
|
|
|
private async saveClineMessages() {
|
|
|
try {
|
|
|
- const taskDir = await this.ensureTaskDirectoryExists()
|
|
|
- const filePath = path.join(taskDir, GlobalFileNames.uiMessages)
|
|
|
- await fs.writeFile(filePath, JSON.stringify(this.clineMessages))
|
|
|
-
|
|
|
- const tokenUsage = this.getTokenUsage()
|
|
|
- this.emit("taskTokenUsageUpdated", this.taskId, tokenUsage)
|
|
|
-
|
|
|
- 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"),
|
|
|
- )
|
|
|
- ]
|
|
|
-
|
|
|
- if (Date.now() - this.taskDirSizeCheckedAt > this.taskDirSizeCheckInterval) {
|
|
|
- this.taskDirSizeCheckedAt = Date.now()
|
|
|
-
|
|
|
- try {
|
|
|
- this.taskDirSize = await getFolderSize.loose(taskDir)
|
|
|
- } catch (err) {
|
|
|
- console.error(
|
|
|
- `[saveClineMessages] failed to get task directory size (${taskDir}): ${err instanceof Error ? err.message : String(err)}`,
|
|
|
- )
|
|
|
- }
|
|
|
- }
|
|
|
+ await saveTaskMessages({
|
|
|
+ messages: this.clineMessages,
|
|
|
+ taskId: this.taskId,
|
|
|
+ globalStoragePath: this.globalStoragePath,
|
|
|
+ })
|
|
|
|
|
|
- await this.providerRef.deref()?.updateTaskHistory({
|
|
|
- id: this.taskId,
|
|
|
- number: this.taskNumber,
|
|
|
- ts: lastRelevantMessage.ts,
|
|
|
- task: taskMessage.text ?? "",
|
|
|
- tokensIn: tokenUsage.totalTokensIn,
|
|
|
- tokensOut: tokenUsage.totalTokensOut,
|
|
|
- cacheWrites: tokenUsage.totalCacheWrites,
|
|
|
- cacheReads: tokenUsage.totalCacheReads,
|
|
|
- totalCost: tokenUsage.totalCost,
|
|
|
- size: this.taskDirSize,
|
|
|
+ const { historyItem, tokenUsage } = await taskMetadata({
|
|
|
+ messages: this.clineMessages,
|
|
|
+ taskId: this.taskId,
|
|
|
+ taskNumber: this.taskNumber,
|
|
|
+ globalStoragePath: this.globalStoragePath,
|
|
|
workspace: this.cwd,
|
|
|
})
|
|
|
+
|
|
|
+ this.emit("taskTokenUsageUpdated", this.taskId, tokenUsage)
|
|
|
+
|
|
|
+ await this.providerRef.deref()?.updateTaskHistory(historyItem)
|
|
|
} catch (error) {
|
|
|
console.error("Failed to save cline messages:", error)
|
|
|
}
|