import { existsSync } from 'node:fs'; import { join } from 'node:path'; import { getAttachmentsDir, getTmpAttachmentsDir } from '../../data-path'; import { getUserById } from 'officerdb'; /** * Where an attached file ended up, so the agent can be told to read it. * * ── Why a path and not the bytes ── * * An image goes to the model as base64 inside the message. A PDF cannot: it would be megabytes of base64 * in the turn, re-sent for the rest of the conversation, to produce text the agent's own tools extract * better. Reading a document is something `Read` already does well — page ranges, and re-reading a * section later without paying for it twice. * * So the file stays where `/upload` put it and the prompt names it. This is a machine where the agent * runs as the person who attached it, so it can simply open it. * * ── The ids are checked, not trusted ── * * An id arrives in a websocket message, and it is used to build a path. `/upload` generates them as * `.`, so anything that is not that shape did not come from us — and a `..` inside one would * name a file outside the attachment directory entirely. Only the two directories uploads actually write * to are searched, and only ids matching the shape uploads actually produce. */ const ATTACHMENT_ID = /^[a-f0-9-]{36}\.[A-Za-z0-9]{1,8}$/; export async function attachedFilePaths( userId: number, sessionId: string, attachmentIds: string[] | undefined, ): Promise { if (!attachmentIds?.length) return []; const user = await getUserById(userId).catch(() => null); if (!user?.email) return []; const paths: string[] = []; for (const id of attachmentIds) { if (!ATTACHMENT_ID.test(id)) continue; // Uploads before a session exists land in `tmp/`; everything after lands under the session. Both are // checked because a first message carries ids from the first shape and later ones from the second. const candidates = [join(getAttachmentsDir(user.email, sessionId), id), join(getTmpAttachmentsDir(user.email), id)]; const found = candidates.find((path) => existsSync(path)); if (found) paths.push(found); } return paths; }