import NodeCache from "node-cache" import getFolderSize from "get-folder-size" import type { ClineMessage, HistoryItem } from "@roo-code/types" import { combineApiRequests } from "../../shared/combineApiRequests" import { combineCommandSequences } from "../../shared/combineCommandSequences" import { getApiMetrics } from "../../shared/getApiMetrics" import { findLastIndex } from "../../shared/array" import { getTaskDirectoryPath } from "../../utils/storage" import { t } from "../../i18n" const taskSizeCache = new NodeCache({ stdTTL: 30, checkperiod: 5 * 60 }) export type TaskMetadataOptions = { taskId: string rootTaskId?: string parentTaskId?: string taskNumber: number messages: ClineMessage[] globalStoragePath: string workspace: string mode?: string /** Initial status for the task (e.g., "active" for child tasks) */ initialStatus?: "active" | "delegated" | "completed" } export async function taskMetadata({ taskId: id, rootTaskId, parentTaskId, taskNumber, messages, globalStoragePath, workspace, mode, initialStatus, }: TaskMetadataOptions) { const taskDir = await getTaskDirectoryPath(globalStoragePath, id) // Determine message availability upfront const hasMessages = messages && messages.length > 0 // Pre-calculate all values based on availability let timestamp: number let tokenUsage: ReturnType let taskDirSize: number let taskMessage: ClineMessage | undefined if (!hasMessages) { // Handle no messages case timestamp = Date.now() tokenUsage = { totalTokensIn: 0, totalTokensOut: 0, totalCacheWrites: 0, totalCacheReads: 0, totalCost: 0, contextTokens: 0, } taskDirSize = 0 } else { // Handle messages case taskMessage = messages[0] // First message is always the task say. const lastRelevantMessage = messages[findLastIndex(messages, (m) => !(m.ask === "resume_task" || m.ask === "resume_completed_task"))] || taskMessage timestamp = lastRelevantMessage.ts tokenUsage = getApiMetrics(combineApiRequests(combineCommandSequences(messages.slice(1)))) // Get task directory size const cachedSize = taskSizeCache.get(taskDir) if (cachedSize === undefined) { try { taskDirSize = await getFolderSize.loose(taskDir) taskSizeCache.set(taskDir, taskDirSize) } catch (error) { taskDirSize = 0 } } else { taskDirSize = cachedSize } } // Create historyItem once with pre-calculated values. // initialStatus is included when provided (e.g., "active" for child tasks) // to ensure the status is set from the very first save, avoiding race conditions // where attempt_completion might run before a separate status update. const historyItem: HistoryItem = { id, rootTaskId, parentTaskId, number: taskNumber, ts: timestamp, task: hasMessages ? taskMessage!.text?.trim() || t("common:tasks.incomplete", { taskNumber }) : t("common:tasks.no_messages", { taskNumber }), tokensIn: tokenUsage.totalTokensIn, tokensOut: tokenUsage.totalTokensOut, cacheWrites: tokenUsage.totalCacheWrites, cacheReads: tokenUsage.totalCacheReads, totalCost: tokenUsage.totalCost, size: taskDirSize, workspace, mode, ...(initialStatus && { status: initialStatus }), } return { historyItem, tokenUsage } }