Skip to content

Commit 0c8aab5

Browse files
committed
Split api and chat message persistence into a separate module
1 parent c6f91a3 commit 0c8aab5

File tree

4 files changed

+157
-78
lines changed

4 files changed

+157
-78
lines changed

src/core/Cline.ts

Lines changed: 21 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import fs from "fs/promises"
21
import * as path from "path"
32
import os from "os"
43
import crypto from "crypto"
@@ -8,7 +7,6 @@ import { Anthropic } from "@anthropic-ai/sdk"
87
import cloneDeep from "clone-deep"
98
import delay from "delay"
109
import pWaitFor from "p-wait-for"
11-
import getFolderSize from "get-folder-size"
1210
import { serializeError } from "serialize-error"
1311
import * as vscode from "vscode"
1412

@@ -58,7 +56,6 @@ import { TerminalRegistry } from "../integrations/terminal/TerminalRegistry"
5856

5957
// utils
6058
import { calculateApiCostAnthropic } from "../utils/cost"
61-
import { fileExistsAtPath } from "../utils/fs"
6259
import { arePathsEqual, getWorkspacePath } from "../utils/path"
6360

6461
// tools
@@ -93,6 +90,7 @@ import { truncateConversationIfNeeded } from "./sliding-window"
9390
import { ClineProvider } from "./webview/ClineProvider"
9491
import { validateToolUse } from "./mode-validator"
9592
import { MultiSearchReplaceDiffStrategy } from "./diff/strategies/multi-search-replace"
93+
import { readApiMessages, saveApiMessages, readTaskMessages, saveTaskMessages } from "./task-persistence"
9694

9795
type UserContent = Array<Anthropic.Messages.ContentBlockParam>
9896

@@ -164,6 +162,7 @@ export class Cline extends EventEmitter<ClineEvents> {
164162
consecutiveMistakeCountForApplyDiff: Map<string, number> = new Map()
165163
// Not private since it needs to be accessible by tools.
166164
providerRef: WeakRef<ClineProvider>
165+
private readonly globalStoragePath: string
167166
private abort: boolean = false
168167
didFinishAbortingStream = false
169168
abandoned = false
@@ -203,7 +202,6 @@ export class Cline extends EventEmitter<ClineEvents> {
203202
task,
204203
images,
205204
historyItem,
206-
experiments,
207205
startTask = true,
208206
rootTask,
209207
parentTask,
@@ -222,9 +220,11 @@ export class Cline extends EventEmitter<ClineEvents> {
222220

223221
this.rooIgnoreController = new RooIgnoreController(this.cwd)
224222
this.fileContextTracker = new FileContextTracker(provider, this.taskId)
223+
225224
this.rooIgnoreController.initialize().catch((error) => {
226225
console.error("Failed to initialize RooIgnoreController:", error)
227226
})
227+
228228
this.apiConfiguration = apiConfiguration
229229
this.api = buildApiHandler(apiConfiguration)
230230
this.urlContentFetcher = new UrlContentFetcher(provider.context)
@@ -234,6 +234,7 @@ export class Cline extends EventEmitter<ClineEvents> {
234234
this.fuzzyMatchThreshold = fuzzyMatchThreshold
235235
this.consecutiveMistakeLimit = consecutiveMistakeLimit
236236
this.providerRef = new WeakRef(provider)
237+
this.globalStoragePath = provider.context.globalStorageUri.fsPath
237238
this.diffViewProvider = new DiffViewProvider(this.cwd)
238239
this.enableCheckpoints = enableCheckpoints
239240

@@ -284,26 +285,8 @@ export class Cline extends EventEmitter<ClineEvents> {
284285

285286
// Storing task to disk for history
286287

287-
private async ensureTaskDirectoryExists(): Promise<string> {
288-
const globalStoragePath = this.providerRef.deref()?.context.globalStorageUri.fsPath
289-
if (!globalStoragePath) {
290-
throw new Error("Global storage uri is invalid")
291-
}
292-
293-
// Use storagePathManager to retrieve the task storage directory
294-
const { getTaskDirectoryPath } = await import("../shared/storagePathManager")
295-
return getTaskDirectoryPath(globalStoragePath, this.taskId)
296-
}
297-
298288
private async getSavedApiConversationHistory(): Promise<Anthropic.MessageParam[]> {
299-
const filePath = path.join(await this.ensureTaskDirectoryExists(), GlobalFileNames.apiConversationHistory)
300-
const fileExists = await fileExistsAtPath(filePath)
301-
302-
if (fileExists) {
303-
return JSON.parse(await fs.readFile(filePath, "utf8"))
304-
}
305-
306-
return []
289+
return readApiMessages({ taskId: this.taskId, globalStoragePath: this.globalStoragePath })
307290
}
308291

309292
private async addToApiConversationHistory(message: Anthropic.MessageParam) {
@@ -319,30 +302,19 @@ export class Cline extends EventEmitter<ClineEvents> {
319302

320303
private async saveApiConversationHistory() {
321304
try {
322-
const filePath = path.join(await this.ensureTaskDirectoryExists(), GlobalFileNames.apiConversationHistory)
323-
await fs.writeFile(filePath, JSON.stringify(this.apiConversationHistory))
305+
await saveApiMessages({
306+
messages: this.apiConversationHistory,
307+
taskId: this.taskId,
308+
globalStoragePath: this.globalStoragePath,
309+
})
324310
} catch (error) {
325311
// in the off chance this fails, we don't want to stop the task
326312
console.error("Failed to save API conversation history:", error)
327313
}
328314
}
329315

330316
private async getSavedClineMessages(): Promise<ClineMessage[]> {
331-
const filePath = path.join(await this.ensureTaskDirectoryExists(), GlobalFileNames.uiMessages)
332-
333-
if (await fileExistsAtPath(filePath)) {
334-
return JSON.parse(await fs.readFile(filePath, "utf8"))
335-
} else {
336-
// check old location
337-
const oldPath = path.join(await this.ensureTaskDirectoryExists(), "claude_messages.json")
338-
if (await fileExistsAtPath(oldPath)) {
339-
const data = JSON.parse(await fs.readFile(oldPath, "utf8"))
340-
await fs.unlink(oldPath) // remove old file
341-
return data
342-
}
343-
}
344-
345-
return []
317+
return readTaskMessages({ taskId: this.taskId, globalStoragePath: this.globalStoragePath })
346318
}
347319

348320
private async addToClineMessages(message: ClineMessage) {
@@ -364,46 +336,17 @@ export class Cline extends EventEmitter<ClineEvents> {
364336

365337
private async saveClineMessages() {
366338
try {
367-
const taskDir = await this.ensureTaskDirectoryExists()
368-
const filePath = path.join(taskDir, GlobalFileNames.uiMessages)
369-
await fs.writeFile(filePath, JSON.stringify(this.clineMessages))
339+
const { historyItem, tokenUsage } = await saveTaskMessages({
340+
messages: this.clineMessages,
341+
taskId: this.taskId,
342+
taskNumber: this.taskNumber,
343+
globalStoragePath: this.globalStoragePath,
344+
workspace: this.cwd,
345+
})
370346

371-
const tokenUsage = this.getTokenUsage()
372347
this.emit("taskTokenUsageUpdated", this.taskId, tokenUsage)
373348

374-
const taskMessage = this.clineMessages[0] // First message is always the task say
375-
376-
const lastRelevantMessage =
377-
this.clineMessages[
378-
findLastIndex(
379-
this.clineMessages,
380-
(m) => !(m.ask === "resume_task" || m.ask === "resume_completed_task"),
381-
)
382-
]
383-
384-
let taskDirSize = 0
385-
386-
try {
387-
taskDirSize = await getFolderSize.loose(taskDir)
388-
} catch (err) {
389-
console.error(
390-
`[saveClineMessages] failed to get task directory size (${taskDir}): ${err instanceof Error ? err.message : String(err)}`,
391-
)
392-
}
393-
394-
await this.providerRef.deref()?.updateTaskHistory({
395-
id: this.taskId,
396-
number: this.taskNumber,
397-
ts: lastRelevantMessage.ts,
398-
task: taskMessage.text ?? "",
399-
tokensIn: tokenUsage.totalTokensIn,
400-
tokensOut: tokenUsage.totalTokensOut,
401-
cacheWrites: tokenUsage.totalCacheWrites,
402-
cacheReads: tokenUsage.totalCacheReads,
403-
totalCost: tokenUsage.totalCost,
404-
size: taskDirSize,
405-
workspace: this.cwd,
406-
})
349+
await this.providerRef.deref()?.updateTaskHistory(historyItem)
407350
} catch (error) {
408351
console.error("Failed to save cline messages:", error)
409352
}
@@ -853,7 +796,7 @@ export class Cline extends EventEmitter<ClineEvents> {
853796
}
854797

855798
const wasRecent = lastClineMessage?.ts && Date.now() - lastClineMessage.ts < 30_000
856-
799+
857800
newUserContent.push({
858801
type: "text",
859802
text:
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import * as path from "path"
2+
import * as fs from "fs/promises"
3+
4+
import { Anthropic } from "@anthropic-ai/sdk"
5+
6+
import { fileExistsAtPath } from "../../utils/fs"
7+
8+
import { GlobalFileNames } from "../../shared/globalFileNames"
9+
import { getTaskDirectoryPath } from "../../shared/storagePathManager"
10+
11+
export type ApiMessage = Anthropic.MessageParam & { ts?: number }
12+
13+
export async function readApiMessages({
14+
taskId,
15+
globalStoragePath,
16+
}: {
17+
taskId: string
18+
globalStoragePath: string
19+
}): Promise<ApiMessage[]> {
20+
const taskDir = await getTaskDirectoryPath(globalStoragePath, taskId)
21+
const filePath = path.join(taskDir, GlobalFileNames.uiMessages)
22+
23+
if (await fileExistsAtPath(filePath)) {
24+
return JSON.parse(await fs.readFile(filePath, "utf8"))
25+
} else {
26+
const oldPath = path.join(taskDir, "claude_messages.json")
27+
28+
if (await fileExistsAtPath(oldPath)) {
29+
const data = JSON.parse(await fs.readFile(oldPath, "utf8"))
30+
await fs.unlink(oldPath)
31+
return data
32+
}
33+
}
34+
35+
return []
36+
}
37+
38+
export async function saveApiMessages({
39+
messages,
40+
taskId,
41+
globalStoragePath,
42+
}: {
43+
messages: ApiMessage[]
44+
taskId: string
45+
globalStoragePath: string
46+
}) {
47+
const taskDir = await getTaskDirectoryPath(globalStoragePath, taskId)
48+
const filePath = path.join(taskDir, GlobalFileNames.apiConversationHistory)
49+
await fs.writeFile(filePath, JSON.stringify(messages))
50+
}

src/core/task-persistence/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { readApiMessages, saveApiMessages } from "./apiMessages"
2+
export { readTaskMessages, saveTaskMessages } from "./taskMessages"
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import * as path from "path"
2+
import * as fs from "fs/promises"
3+
4+
import getFolderSize from "get-folder-size"
5+
6+
import { fileExistsAtPath } from "../../utils/fs"
7+
8+
import { GlobalFileNames } from "../../shared/globalFileNames"
9+
import { ClineMessage } from "../../shared/ExtensionMessage"
10+
import { combineApiRequests } from "../../shared/combineApiRequests"
11+
import { combineCommandSequences } from "../../shared/combineCommandSequences"
12+
import { getApiMetrics } from "../../shared/getApiMetrics"
13+
import { findLastIndex } from "../../shared/array"
14+
import { HistoryItem } from "../../shared/HistoryItem"
15+
import { getTaskDirectoryPath } from "../../shared/storagePathManager"
16+
17+
export async function readTaskMessages({
18+
taskId,
19+
globalStoragePath,
20+
}: {
21+
taskId: string
22+
globalStoragePath: string
23+
}): Promise<ClineMessage[]> {
24+
const taskDir = await getTaskDirectoryPath(globalStoragePath, taskId)
25+
const filePath = path.join(taskDir, GlobalFileNames.apiConversationHistory)
26+
const fileExists = await fileExistsAtPath(filePath)
27+
28+
if (fileExists) {
29+
return JSON.parse(await fs.readFile(filePath, "utf8"))
30+
}
31+
32+
return []
33+
}
34+
35+
export async function saveTaskMessages({
36+
messages,
37+
taskId,
38+
taskNumber,
39+
globalStoragePath,
40+
workspace,
41+
}: {
42+
messages: ClineMessage[]
43+
taskId: string
44+
taskNumber: number
45+
globalStoragePath: string
46+
workspace: string
47+
}) {
48+
const taskDir = await getTaskDirectoryPath(globalStoragePath, taskId)
49+
const filePath = path.join(taskDir, GlobalFileNames.uiMessages)
50+
await fs.writeFile(filePath, JSON.stringify(messages))
51+
52+
const taskMessage = messages[0] // First message is always the task say.
53+
54+
const lastRelevantMessage =
55+
messages[findLastIndex(messages, (m) => !(m.ask === "resume_task" || m.ask === "resume_completed_task"))]
56+
57+
let taskDirSize = 0
58+
59+
try {
60+
taskDirSize = await getFolderSize.loose(taskDir)
61+
} catch (error) {
62+
console.error(
63+
`[saveTaskMessages] getFolderSize.loose failed -> ${error instanceof Error ? error.message : String(error)}`,
64+
)
65+
}
66+
67+
const tokenUsage = getApiMetrics(combineApiRequests(combineCommandSequences(messages.slice(1))))
68+
69+
const historyItem: HistoryItem = {
70+
id: taskId,
71+
number: taskNumber,
72+
ts: lastRelevantMessage.ts,
73+
task: taskMessage.text ?? "",
74+
tokensIn: tokenUsage.totalTokensIn,
75+
tokensOut: tokenUsage.totalTokensOut,
76+
cacheWrites: tokenUsage.totalCacheWrites,
77+
cacheReads: tokenUsage.totalCacheReads,
78+
totalCost: tokenUsage.totalCost,
79+
size: taskDirSize,
80+
workspace,
81+
}
82+
83+
return { historyItem, tokenUsage }
84+
}

0 commit comments

Comments
 (0)