From ba3b89ce08ffb4e564fcf53d6f853e53c480e02d Mon Sep 17 00:00:00 2001 From: "posthog[bot]" <206114724+posthog[bot]@users.noreply.github.com> Date: Fri, 10 Jul 2026 20:22:39 +0000 Subject: [PATCH] fix(workspaces): make upload name resolution and insert atomic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Concurrent uploads of the same filename to the same parent could fail with a SQLite UNIQUE constraint error on kernel_items.name. The workspace kernel is a Durable Object, so concurrent RPC calls interleave at `await` points. Because `resolveItemName` and the `INSERT` were separated by the awaited `writeFileBytes` / `r2.delete` calls, two simultaneous uploads could both resolve to the same collision-free name (neither inserted yet), then the second insert collided on the partial unique index — a check-then-act (TOCTOU) race. Move the awaited file/R2 work before name resolution so `resolveItemName` and the synchronous `INSERT` run in the same event-loop turn with no suspension between them. `shellPath` and metadata are derived from `itemId`/`requestedName`, not the resolved `name`, so reordering is safe. Generated-By: PostHog Code Task-Id: 888b423f-9eae-481d-b17d-49619571f76f --- .../kernel/workspace-kernel-file-commands.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/features/workspaces/kernel/workspace-kernel-file-commands.ts b/src/features/workspaces/kernel/workspace-kernel-file-commands.ts index e23f733c..76e52206 100644 --- a/src/features/workspaces/kernel/workspace-kernel-file-commands.ts +++ b/src/features/workspaces/kernel/workspace-kernel-file-commands.ts @@ -86,12 +86,6 @@ export class WorkspaceKernelFileCommands { const now = Date.now(); const itemId = crypto.randomUUID(); const requestedName = normalizeWorkspaceUploadFileName(input.fileName, descriptor); - const name = this.store.resolveItemName({ - itemId, - type: "file", - parentId, - requestedName, - }); const shellPath = getWorkspaceKernelFileShellPath({ itemId, extension: getWorkspaceFileShellExtension({ @@ -111,6 +105,19 @@ export class WorkspaceKernelFileCommands { await this.workspace.writeFileBytes(shellPath, bytes, contentType); await this.r2.delete(input.objectKey); + // Resolve the collision-free name and INSERT in the same synchronous turn. + // The workspace kernel is a Durable Object, so concurrent uploads only + // interleave at `await` points; keeping name resolution and the insert + // adjacent (no `await` between them) prevents two simultaneous uploads of + // the same filename from both resolving to the same name and colliding on + // the partial unique index (kernel_items.name). See resolveItemName below. + const name = this.store.resolveItemName({ + itemId, + type: "file", + parentId, + requestedName, + }); + this.sql` INSERT INTO kernel_items ( id,