Skip to content

Commit c1c4160

Browse files
revert(mothership): back out function_execute table-mount drain — defer to follow-up
The keyset CSV drain for inputTables fixed the 100-row truncation but introduced an unbounded in-memory materialization: a large table (e.g. enterprise 1M-row) is drained whole into the web-container heap before the 50MB mount check runs, risking OOM. Reverting to main's behavior (100-row mount) for now; the proper fix (incremental byte-bound + filter/limit/columns selection, or by-reference signed-URL fetch) is deferred to a separate effort so this PR can ship the user_table speed work (C/D/E) without the memory regression. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 7310c06 commit c1c4160

2 files changed

Lines changed: 46 additions & 208 deletions

File tree

apps/sim/lib/copilot/tools/handlers/function-execute.test.ts

Lines changed: 0 additions & 135 deletions
This file was deleted.

apps/sim/lib/copilot/tools/handlers/function-execute.ts

Lines changed: 46 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,7 @@ import { decodeVfsPathSegments, encodeVfsPathSegments } from '@/lib/copilot/vfs/
33
import { resolveWorkflowAliasForWorkspace } from '@/lib/copilot/vfs/workflow-alias-resolver'
44
import { isPlanAliasPath, workflowAliasSandboxPath } from '@/lib/copilot/vfs/workflow-aliases'
55
import { isMothershipBetaFeaturesEnabled } from '@/lib/core/config/feature-flags'
6-
import { buildNameById, rowDataIdToName } from '@/lib/table/column-keys'
7-
import { toCsvRow } from '@/lib/table/export-format'
8-
import { getTableById, listTables, selectExportRowPage } from '@/lib/table/service'
9-
import type { TableDefinition } from '@/lib/table/types'
6+
import { getTableById, listTables, queryRows } from '@/lib/table/service'
107
import { listWorkspaceFileFolders } from '@/lib/uploads/contexts/workspace/workspace-file-folder-manager'
118
import {
129
fetchWorkspaceFileBuffer,
@@ -65,44 +62,6 @@ async function resolveTableRef(
6562
return tablePathLookup?.get(tableName) ?? null
6663
}
6764

68-
const TABLE_MOUNT_PAGE_SIZE = 5000
69-
70-
/**
71-
* Serializes a cell for a sandbox CSV mount. Unlike export downloads this skips
72-
* formula neutralization — the CSV is consumed by code, and a prefixed `'`
73-
* would corrupt values.
74-
*/
75-
function formatMountCsvValue(value: unknown): string {
76-
if (value === null || value === undefined) return ''
77-
if (value instanceof Date) return value.toISOString()
78-
if (typeof value === 'object') return JSON.stringify(value)
79-
return String(value)
80-
}
81-
82-
/**
83-
* Serializes a full table to CSV for a sandbox mount. Walks the keyset export
84-
* reader page by page so every row is included (`queryRows` with defaults
85-
* silently truncated mounts to its 100-row page and paid for a count and
86-
* execution metadata the CSV never used), and remaps stored column-id keys
87-
* back to display names so headers line up with cell values.
88-
*/
89-
async function buildTableCsvForMount(table: TableDefinition): Promise<string> {
90-
const nameById = buildNameById(table.schema)
91-
const headers = table.schema.columns.map((c) => c.name)
92-
const lines = [toCsvRow(headers)]
93-
let after: { position: number; id: string } | null = null
94-
while (true) {
95-
const page = await selectExportRowPage(table, after, TABLE_MOUNT_PAGE_SIZE)
96-
for (const row of page) {
97-
const data = rowDataIdToName(row.data, nameById)
98-
lines.push(toCsvRow(headers.map((header) => formatMountCsvValue(data[header]))))
99-
}
100-
if (page.length < TABLE_MOUNT_PAGE_SIZE) return lines.join('\n')
101-
const last = page[page.length - 1]
102-
after = { position: last.position, id: last.id }
103-
}
104-
}
105-
10665
async function resolveInputFiles(
10766
workspaceId: string,
10867
inputFiles?: unknown[],
@@ -288,41 +247,55 @@ async function resolveInputFiles(
288247
const tablePathLookup = hasTablePathRefs
289248
? new Map((await listTables(workspaceId)).map((table) => [table.name, table]))
290249
: undefined
291-
const tableMounts = await Promise.all(
292-
inputTables.map(async (tableRef) => {
293-
const tableId =
294-
typeof tableRef === 'string'
295-
? tableRef
296-
: tableRef && typeof tableRef === 'object'
297-
? (tableRef as CanonicalTableInput).tableId || (tableRef as CanonicalTableInput).path
298-
: undefined
299-
if (!tableId) return null
300-
const table = await resolveTableRef(tableId, tablePathLookup)
301-
if (!table || table.workspaceId !== workspaceId) {
302-
throw new Error(
303-
`Input table not found: "${tableId}". Pass the table id (tbl_...) from tables/{name}/meta.json, or a tables/{name}/meta.json path.`
304-
)
305-
}
306-
const csvContent = await buildTableCsvForMount(table)
307-
const sandboxPath =
308-
typeof tableRef === 'object' && tableRef !== null
309-
? (tableRef as CanonicalTableInput).sandboxPath
250+
for (const tableRef of inputTables) {
251+
const tableId =
252+
typeof tableRef === 'string'
253+
? tableRef
254+
: tableRef && typeof tableRef === 'object'
255+
? (tableRef as CanonicalTableInput).tableId || (tableRef as CanonicalTableInput).path
310256
: undefined
311-
return {
312-
path: sandboxPath || `/home/user/tables/${table.id}.csv`,
313-
content: csvContent,
314-
}
315-
})
316-
)
317-
for (const mount of tableMounts) {
318-
if (!mount) continue
319-
if (totalSize + mount.content.length > MAX_TOTAL_SIZE) {
257+
if (!tableId) continue
258+
const table = await resolveTableRef(tableId, tablePathLookup)
259+
if (!table || table.workspaceId !== workspaceId) {
320260
throw new Error(
321-
`Mounting table "${mount.path}" would exceed the ${MAX_TOTAL_SIZE / 1024 / 1024}MB total mount limit. Mount fewer or smaller tables.`
261+
`Input table not found: "${tableId}". Pass the table id (tbl_...) from tables/{name}/meta.json, or a tables/{name}/meta.json path.`
262+
)
263+
}
264+
const rows = await queryRows(table, {}, 'copilot-fn-exec')
265+
266+
const allKeys = new Set(table.schema.columns.map((column) => column.name))
267+
for (const row of rows.rows ?? []) {
268+
if (row.data && typeof row.data === 'object') {
269+
for (const key of Object.keys(row.data as Record<string, unknown>)) {
270+
allKeys.add(key)
271+
}
272+
}
273+
}
274+
const headers = Array.from(allKeys)
275+
const csvLines = [headers.join(',')]
276+
for (const row of rows.rows ?? []) {
277+
const data = (row.data || {}) as Record<string, unknown>
278+
csvLines.push(
279+
headers
280+
.map((h) => {
281+
const val = data[h]
282+
const str = val === null || val === undefined ? '' : String(val)
283+
return str.includes(',') || str.includes('"') || str.includes('\n')
284+
? `"${str.replace(/"/g, '""')}"`
285+
: str
286+
})
287+
.join(',')
322288
)
323289
}
324-
totalSize += mount.content.length
325-
sandboxFiles.push(mount)
290+
const csvContent = csvLines.join('\n')
291+
const sandboxPath =
292+
typeof tableRef === 'object' && tableRef !== null
293+
? (tableRef as CanonicalTableInput).sandboxPath
294+
: undefined
295+
sandboxFiles.push({
296+
path: sandboxPath || `/home/user/tables/${table.id}.csv`,
297+
content: csvContent,
298+
})
326299
}
327300
}
328301

0 commit comments

Comments
 (0)