Skip to content

Commit 339f192

Browse files
committed
fix(files): bound file version retention per run and document both coalescing cutoffs
1 parent eaa9ff0 commit 339f192

4 files changed

Lines changed: 24 additions & 7 deletions

File tree

‎apps/docs/openapi-v2-files-audit.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -868,7 +868,7 @@
868868
"get": {
869869
"operationId": "listFileVersions",
870870
"summary": "List File Versions",
871-
"description": "List the versions of a file, newest first by default. Each write that changes the bytes records one; identical rewrites do not. Collaborative edits within ten minutes fold into one version, as do repeated workflow writes by one author. Renames and moves are not versions. An empty file is version 1 until its first content replaces it. Retention keeps the newest ten and removes older versions by plan, so numbers can have gaps.\n\nOAuth scope: `api:read`.",
871+
"description": "List the versions of a file, newest first by default. Each write that changes the bytes records one; identical rewrites do not. Collaborative edits, and repeated workflow writes by one author, fold into the current version while it is under ten minutes old and written in the last five. Renames and moves are not versions. An empty file is version 1 until its first content replaces it. Retention keeps the newest ten, so numbers can have gaps.\n\nOAuth scope: `api:read`.",
872872
"x-sim-operation": "files.versions.list",
873873
"x-oauth-scope": "api:read",
874874
"tags": ["Files"],

‎apps/sim/background/cleanup-file-versions.ts‎

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { task } from '@trigger.dev/sdk'
66
import { and, count, gt, inArray, isNotNull, lt, min, or, sql } from 'drizzle-orm'
77
import type { CleanupJobPayload } from '@/lib/billing/cleanup-dispatcher'
88
import {
9+
DEFAULT_BATCH_SIZE,
910
DEFAULT_DELETE_CHUNK_SIZE,
1011
DEFAULT_MAX_BATCHES_PER_TABLE,
1112
DEFAULT_WORKSPACE_CHUNK_SIZE,
@@ -22,6 +23,12 @@ const cleanupDb = dbFor('cleanup')
2223
/** Candidate files whose histories are ranked in one query. */
2324
const FILES_PER_QUERY = 500
2425

26+
/**
27+
* Bounds one run like the other cleanup jobs: {@link DEFAULT_MAX_BATCHES_PER_TABLE} batches per
28+
* workspace chunk and this many versions overall. The next run resumes where this one stopped.
29+
*/
30+
const MAX_VERSIONS_PER_RUN = DEFAULT_BATCH_SIZE * DEFAULT_MAX_BATCHES_PER_TABLE
31+
2532
/**
2633
* Superseded versions a free file keeps (its newest 100 with the current one); versions beyond it
2734
* are pruned whatever their age. Paid plans are bounded only by the inline write-time ceiling.
@@ -146,16 +153,26 @@ export async function runCleanupFileVersions(payload: CleanupJobPayload): Promis
146153
)
147154

148155
let deleted = 0
156+
let attempted = 0
149157
for (const group of chunkArray(workspaceIds, DEFAULT_WORKSPACE_CHUNK_SIZE)) {
158+
if (attempted >= MAX_VERSIONS_PER_RUN) break
150159
const candidates = await selectCandidateFileIds(group, cutoff, maxSuperseded)
160+
let batches = 0
151161
for (const fileIds of chunkArray(candidates, FILES_PER_QUERY)) {
152-
for (let batch = 0; batch < DEFAULT_MAX_BATCHES_PER_TABLE; batch++) {
162+
let exhausted = false
163+
while (
164+
!exhausted &&
165+
batches < DEFAULT_MAX_BATCHES_PER_TABLE &&
166+
attempted < MAX_VERSIONS_PER_RUN
167+
) {
168+
batches++
153169
const expired = await selectExpiredVersions(fileIds, cutoff, maxSuperseded)
154-
if (expired.length === 0) break
155-
const removed = await deleteVersions(expired)
170+
attempted += expired.length
171+
const removed = expired.length > 0 ? await deleteVersions(expired) : 0
156172
deleted += removed
157-
if (expired.length < DEFAULT_DELETE_CHUNK_SIZE || removed === 0) break
173+
exhausted = expired.length < DEFAULT_DELETE_CHUNK_SIZE || removed === 0
158174
}
175+
if (!exhausted) break
159176
}
160177
}
161178

‎apps/sim/lib/api/contracts/v2/openapi/files-audit.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -447,7 +447,7 @@ const declaredRoutes = [
447447
operationId: 'listFileVersions',
448448
summary: 'List File Versions',
449449
description:
450-
'List the versions of a file, newest first by default. Each write that changes the bytes records one; identical rewrites do not. Collaborative edits within ten minutes fold into one version, as do repeated workflow writes by one author. Renames and moves are not versions. An empty file is version 1 until its first content replaces it. Retention keeps the newest ten and removes older versions by plan, so numbers can have gaps.',
450+
'List the versions of a file, newest first by default. Each write that changes the bytes records one; identical rewrites do not. Collaborative edits, and repeated workflow writes by one author, fold into the current version while it is under ten minutes old and written in the last five. Renames and moves are not versions. An empty file is version 1 until its first content replaces it. Retention keeps the newest ten, so numbers can have gaps.',
451451
errors: RESOURCE_ERRORS,
452452
success: { description: 'A page of file versions.' },
453453
}),

‎apps/sim/lib/api/mcp/generated/v2-operations.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1425,7 +1425,7 @@ export const V2_MCP_OPERATIONS = {
14251425
contract: v2ListFileVersionsContract,
14261426
summary: 'List File Versions',
14271427
description:
1428-
'List the versions of a file, newest first by default. Each write that changes the bytes records one; identical rewrites do not. Collaborative edits within ten minutes fold into one version, as do repeated workflow writes by one author. Renames and moves are not versions. An empty file is version 1 until its first content replaces it. Retention keeps the newest ten and removes older versions by plan, so numbers can have gaps.\n\nOAuth scope: `api:read`.',
1428+
'List the versions of a file, newest first by default. Each write that changes the bytes records one; identical rewrites do not. Collaborative edits, and repeated workflow writes by one author, fold into the current version while it is under ten minutes old and written in the last five. Renames and moves are not versions. An empty file is version 1 until its first content replaces it. Retention keeps the newest ten, so numbers can have gaps.\n\nOAuth scope: `api:read`.',
14291429
handler: () => import('@/app/api/v2/files/[fileId]/versions/route').then((route) => route.GET),
14301430
},
14311431
listKnowledgeBases: {

0 commit comments

Comments
 (0)