Skip to content

Commit 0647e0b

Browse files
committed
fix(knowledge): stop capping the unpaged knowledge-base list
#6770 routed GET /api/knowledge through the paged workspace read, which fails hard above MAX_KNOWLEDGE_BASES_PER_WORKSPACE. That cap is right for a paged caller — it can be told the set outgrew its page — but this surface has no cursor and its callers each want the whole set, so the cap could only ever mean a 500. Nothing prunes archived knowledge bases, so a long-lived workspace crosses any fixed row count on its own. Staging did: the first list request after the deploy returned 500 with "Knowledge base list exceeds the 10000 row limit", against data that had served fine for days. Read unbounded here, matching the sibling internal lists (`listTables`, workspace files), and leave the cap on the paged read where it was designed to live. The legacy personal read loses its cap for the same reason — it feeds the same unpaged surface.
1 parent 11fe848 commit 0647e0b

3 files changed

Lines changed: 46 additions & 38 deletions

File tree

apps/sim/lib/knowledge/constants.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,6 @@ export const KNOWLEDGE_BASE_DESCRIPTION_MAX_LENGTH = 10_000
55
/** Hard bound for full-workspace knowledge-base list projections. */
66
export const MAX_KNOWLEDGE_BASES_PER_WORKSPACE = 10_000
77

8-
/**
9-
* Cap on one caller's legacy workspace-less knowledge bases. Separate from the per-workspace
10-
* cap because it bounds a per-user set governed by no workspace rule — the two limits should
11-
* be free to move independently.
12-
*/
13-
export const MAX_LEGACY_PERSONAL_KNOWLEDGE_BASES = 10_000
148
/** Hard bound for path-indexed knowledge folder trees and recursive cascades. */
159
export const MAX_KNOWLEDGE_FOLDERS_PER_WORKSPACE = MAX_FOLDERS_PER_WORKSPACE
1610

apps/sim/lib/knowledge/service.test.ts

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -110,18 +110,6 @@ describe('getLegacyPersonalKnowledgeBases', () => {
110110
expect(joinedTables).toContain(schemaMock.document)
111111
expect(joinedTables).not.toContain(schemaMock.permissions)
112112
})
113-
114-
it('fails before projecting connector data for an oversized set', async () => {
115-
dbChainMockFns.limit.mockResolvedValueOnce(
116-
Array.from({ length: MAX_KNOWLEDGE_BASES_PER_WORKSPACE + 1 }, (_, index) => ({
117-
id: `kb-${index}`,
118-
}))
119-
)
120-
121-
await expect(getLegacyPersonalKnowledgeBases('user-a')).rejects.toThrow(
122-
`Legacy personal knowledge base list exceeds the ${MAX_KNOWLEDGE_BASES_PER_WORKSPACE} row limit`
123-
)
124-
})
125113
})
126114

127115
/**
@@ -135,6 +123,27 @@ describe('listWorkspaceAndLegacyKnowledgeBases', () => {
135123
resetDbChainMock()
136124
})
137125

126+
/**
127+
* Nothing prunes archived rows, so a long-lived workspace reaches any fixed row cap on its
128+
* own. This surface has no cursor to page with, so a cap here can only mean a 500 on the
129+
* knowledge page and Recently Deleted — which is exactly what it meant on staging. The cap
130+
* belongs to the paged read, where a caller can be told the set outgrew its page.
131+
*/
132+
it('serves a workspace whose archived set is larger than the paged read’s cap', async () => {
133+
const rows = Array.from({ length: MAX_KNOWLEDGE_BASES_PER_WORKSPACE + 1 }, (_, index) => ({
134+
id: `kb-${index}`,
135+
chunkingConfig: {},
136+
docCount: 0,
137+
createdAt: new Date('2026-01-01T00:00:00Z'),
138+
}))
139+
dbChainMockFns.orderBy.mockResolvedValueOnce(rows).mockResolvedValueOnce([])
140+
dbChainMockFns.limit.mockResolvedValueOnce([])
141+
142+
const result = await listWorkspaceAndLegacyKnowledgeBases('user-a', 'ws-1', 'archived')
143+
144+
expect(result).toHaveLength(MAX_KNOWLEDGE_BASES_PER_WORKSPACE + 1)
145+
})
146+
138147
it('orders both sources as one list and projects connectors once', async () => {
139148
const workspaceRow = {
140149
id: 'kb-workspace',
@@ -148,16 +157,16 @@ describe('listWorkspaceAndLegacyKnowledgeBases', () => {
148157
docCount: 0,
149158
createdAt: new Date('2025-01-01T00:00:00Z'),
150159
}
151-
dbChainMockFns.limit
152-
.mockResolvedValueOnce([workspaceRow])
153-
.mockResolvedValueOnce([legacyRow])
154-
.mockResolvedValueOnce([])
160+
/** Both row reads are unbounded now, so each resolves at `orderBy`; only the connector
161+
* projection still ends in `limit`. */
162+
dbChainMockFns.orderBy.mockResolvedValueOnce([workspaceRow]).mockResolvedValueOnce([legacyRow])
163+
dbChainMockFns.limit.mockResolvedValueOnce([])
155164

156165
const result = await listWorkspaceAndLegacyKnowledgeBases('user-a', 'ws-1')
157166

158167
expect(result.map((kb) => kb.id)).toEqual(['kb-legacy', 'kb-workspace'])
159-
/** Two row reads and ONE connector projection — three chains, never four. */
160-
expect(dbChainMockFns.limit).toHaveBeenCalledTimes(3)
168+
/** ONE connector projection over the merged set, not one per source. */
169+
expect(dbChainMockFns.limit).toHaveBeenCalledTimes(1)
161170
})
162171
})
163172

apps/sim/lib/knowledge/service.ts

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ import { findActiveFolder, resolveRestoredFolderId } from '@/lib/folders/queries
3131
import {
3232
MAX_KNOWLEDGE_BASES_PER_WORKSPACE,
3333
MAX_KNOWLEDGE_CONNECTOR_TYPE_ROWS_PER_LIST,
34-
MAX_LEGACY_PERSONAL_KNOWLEDGE_BASES,
3534
} from '@/lib/knowledge/constants'
3635
import type {
3736
ChunkingConfig,
@@ -181,9 +180,9 @@ function knowledgeBaseScopeCondition(scope: KnowledgeBaseScope) {
181180
async function readKnowledgeBaseRows(
182181
where: SQL | undefined,
183182
orderBy: SQL[],
184-
limit: number
183+
limit?: number
185184
): Promise<Array<Omit<KnowledgeBaseWithCounts, 'connectorTypes'>>> {
186-
const rows = await db
185+
const query = db
187186
.select({
188187
id: knowledgeBase.id,
189188
userId: knowledgeBase.userId,
@@ -213,7 +212,8 @@ async function readKnowledgeBaseRows(
213212
.where(where)
214213
.groupBy(knowledgeBase.id)
215214
.orderBy(...orderBy)
216-
.limit(limit)
215+
216+
const rows = limit === undefined ? await query : await query.limit(limit)
217217

218218
return rows.map((kb) => ({
219219
...kb,
@@ -348,17 +348,9 @@ async function readLegacyPersonalKnowledgeBaseRows(
348348
eq(knowledgeBase.userId, userId),
349349
isNull(knowledgeBase.workspaceId)
350350
),
351-
listOrderBy(keysetColumns(KNOWLEDGE_BASE_SORTS.createdAt), 'asc'),
352-
MAX_LEGACY_PERSONAL_KNOWLEDGE_BASES + 1
351+
listOrderBy(keysetColumns(KNOWLEDGE_BASE_SORTS.createdAt), 'asc')
353352
)
354353

355-
/** One row past the cap, so an oversized set fails loudly instead of truncating in silence. */
356-
if (rows.length > MAX_LEGACY_PERSONAL_KNOWLEDGE_BASES) {
357-
throw new Error(
358-
`Legacy personal knowledge base list exceeds the ${MAX_LEGACY_PERSONAL_KNOWLEDGE_BASES} row limit`
359-
)
360-
}
361-
362354
return rows
363355
}
364356

@@ -386,8 +378,21 @@ export async function listWorkspaceAndLegacyKnowledgeBases(
386378
workspaceId: string,
387379
scope: KnowledgeBaseScope = 'active'
388380
): Promise<KnowledgeBaseWithCounts[]> {
381+
/**
382+
* Unbounded on purpose, matching the sibling internal lists (`listTables`,
383+
* `listWorkspaceFiles`): this surface has no cursor to page with and its callers — the
384+
* knowledge page, the base selector, Recently Deleted — each want the whole set.
385+
*
386+
* `MAX_KNOWLEDGE_BASES_PER_WORKSPACE` guards the PAGED read instead, where a caller that
387+
* asked for a page can be told the set outgrew it. Applying it here turned a slow list into
388+
* a 500 for any workspace that had ever archived more knowledge bases than the cap, which is
389+
* a state archived rows reach on their own, since nothing prunes them.
390+
*/
389391
const [workspaceRows, legacyPersonalRows] = await Promise.all([
390-
readWorkspaceKnowledgeBaseRows(workspaceId, scope).then((page) => page.data),
392+
readKnowledgeBaseRows(
393+
and(eq(knowledgeBase.workspaceId, workspaceId), knowledgeBaseScopeCondition(scope)),
394+
listOrderBy(keysetColumns(KNOWLEDGE_BASE_SORTS.createdAt), 'asc')
395+
),
391396
readLegacyPersonalKnowledgeBaseRows(userId, scope),
392397
])
393398

0 commit comments

Comments
 (0)