(payload: CleanupJobPayload)
| 67 | } |
| 68 | |
| 69 | export async function runCleanupTasks(payload: CleanupJobPayload): Promise<void> { |
| 70 | const startTime = Date.now() |
| 71 | const { workspaceIds, retentionHours, label } = payload |
| 72 | |
| 73 | if (workspaceIds.length === 0) { |
| 74 | logger.info(`[${label}] No workspaces to process`) |
| 75 | return |
| 76 | } |
| 77 | |
| 78 | const retentionDate = new Date(Date.now() - retentionHours * 60 * 60 * 1000) |
| 79 | logger.info( |
| 80 | `[${label}] Processing ${workspaceIds.length} workspaces, cutoff: ${retentionDate.toISOString()}` |
| 81 | ) |
| 82 | |
| 83 | const doomedChats = await selectRowsByIdChunks(workspaceIds, (chunkIds, chunkLimit) => |
| 84 | db |
| 85 | .select({ id: copilotChats.id }) |
| 86 | .from(copilotChats) |
| 87 | .where( |
| 88 | and(inArray(copilotChats.workspaceId, chunkIds), lt(copilotChats.updatedAt, retentionDate)) |
| 89 | ) |
| 90 | .limit(chunkLimit) |
| 91 | ) |
| 92 | |
| 93 | const doomedChatIds = doomedChats.map((c) => c.id) |
| 94 | |
| 95 | // Prepare chat cleanup (collect file keys + copilot backend call) BEFORE DB deletion |
| 96 | const chatCleanup = await prepareChatCleanup(doomedChatIds, label) |
| 97 | |
| 98 | // Delete run children first (checkpoints, tool calls) since they reference runs |
| 99 | const runChildResults = await cleanupRunChildren(workspaceIds, retentionDate, label) |
| 100 | for (const r of runChildResults) { |
| 101 | if (r.deleted > 0) logger.info(`[${r.table}] ${r.deleted} deleted`) |
| 102 | } |
| 103 | |
| 104 | // Delete feedback — no direct workspaceId, reuse chat IDs collected above |
| 105 | const feedbackResult = await deleteRowsById( |
| 106 | copilotFeedback, |
| 107 | copilotFeedback.chatId, |
| 108 | doomedChatIds, |
| 109 | `${label}/copilotFeedback` |
| 110 | ) |
| 111 | |
| 112 | // Delete copilot runs (has workspaceId directly, cascades checkpoints) |
| 113 | const runsResult = await batchDeleteByWorkspaceAndTimestamp({ |
| 114 | tableDef: copilotRuns, |
| 115 | workspaceIdCol: copilotRuns.workspaceId, |
| 116 | timestampCol: copilotRuns.updatedAt, |
| 117 | workspaceIds, |
| 118 | retentionDate, |
| 119 | tableName: `${label}/copilotRuns`, |
| 120 | }) |
| 121 | |
| 122 | // Delete copilot chats using the exact IDs collected above so the chat |
| 123 | // cleanup (S3 + copilot backend) and the DB delete can never disagree. |
| 124 | const chatsResult = await deleteRowsById( |
| 125 | copilotChats, |
| 126 | copilotChats.id, |
nothing calls this directly
no test coverage detected