taskMetadata.ts 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
  1. import NodeCache from "node-cache"
  2. import getFolderSize from "get-folder-size"
  3. import type { ClineMessage, HistoryItem } from "@roo-code/types"
  4. import { combineApiRequests } from "../../shared/combineApiRequests"
  5. import { combineCommandSequences } from "../../shared/combineCommandSequences"
  6. import { getApiMetrics } from "../../shared/getApiMetrics"
  7. import { findLastIndex } from "../../shared/array"
  8. import { getTaskDirectoryPath } from "../../utils/storage"
  9. import { t } from "../../i18n"
  10. const taskSizeCache = new NodeCache({ stdTTL: 30, checkperiod: 5 * 60 })
  11. export type TaskMetadataOptions = {
  12. taskId: string
  13. rootTaskId?: string
  14. parentTaskId?: string
  15. taskNumber: number
  16. messages: ClineMessage[]
  17. globalStoragePath: string
  18. workspace: string
  19. mode?: string
  20. /** Initial status for the task (e.g., "active" for child tasks) */
  21. initialStatus?: "active" | "delegated" | "completed"
  22. }
  23. export async function taskMetadata({
  24. taskId: id,
  25. rootTaskId,
  26. parentTaskId,
  27. taskNumber,
  28. messages,
  29. globalStoragePath,
  30. workspace,
  31. mode,
  32. initialStatus,
  33. }: TaskMetadataOptions) {
  34. const taskDir = await getTaskDirectoryPath(globalStoragePath, id)
  35. // Determine message availability upfront
  36. const hasMessages = messages && messages.length > 0
  37. // Pre-calculate all values based on availability
  38. let timestamp: number
  39. let tokenUsage: ReturnType<typeof getApiMetrics>
  40. let taskDirSize: number
  41. let taskMessage: ClineMessage | undefined
  42. if (!hasMessages) {
  43. // Handle no messages case
  44. timestamp = Date.now()
  45. tokenUsage = {
  46. totalTokensIn: 0,
  47. totalTokensOut: 0,
  48. totalCacheWrites: 0,
  49. totalCacheReads: 0,
  50. totalCost: 0,
  51. contextTokens: 0,
  52. }
  53. taskDirSize = 0
  54. } else {
  55. // Handle messages case
  56. taskMessage = messages[0] // First message is always the task say.
  57. const lastRelevantMessage =
  58. messages[findLastIndex(messages, (m) => !(m.ask === "resume_task" || m.ask === "resume_completed_task"))] ||
  59. taskMessage
  60. timestamp = lastRelevantMessage.ts
  61. tokenUsage = getApiMetrics(combineApiRequests(combineCommandSequences(messages.slice(1))))
  62. // Get task directory size
  63. const cachedSize = taskSizeCache.get<number>(taskDir)
  64. if (cachedSize === undefined) {
  65. try {
  66. taskDirSize = await getFolderSize.loose(taskDir)
  67. taskSizeCache.set<number>(taskDir, taskDirSize)
  68. } catch (error) {
  69. taskDirSize = 0
  70. }
  71. } else {
  72. taskDirSize = cachedSize
  73. }
  74. }
  75. // Create historyItem once with pre-calculated values.
  76. // initialStatus is included when provided (e.g., "active" for child tasks)
  77. // to ensure the status is set from the very first save, avoiding race conditions
  78. // where attempt_completion might run before a separate status update.
  79. const historyItem: HistoryItem = {
  80. id,
  81. rootTaskId,
  82. parentTaskId,
  83. number: taskNumber,
  84. ts: timestamp,
  85. task: hasMessages
  86. ? taskMessage!.text?.trim() || t("common:tasks.incomplete", { taskNumber })
  87. : t("common:tasks.no_messages", { taskNumber }),
  88. tokensIn: tokenUsage.totalTokensIn,
  89. tokensOut: tokenUsage.totalTokensOut,
  90. cacheWrites: tokenUsage.totalCacheWrites,
  91. cacheReads: tokenUsage.totalCacheReads,
  92. totalCost: tokenUsage.totalCost,
  93. size: taskDirSize,
  94. workspace,
  95. mode,
  96. ...(initialStatus && { status: initialStatus }),
  97. }
  98. return { historyItem, tokenUsage }
  99. }