hydrate files from thread root message on replies

When replying to a Slack thread, files attached to the root message were
  not being fetched. The existing `resolveSlackThreadStarter()` fetched the
  root message text via `conversations.replies` but ignored the `files[]`
  array in the response.

  Changes:
  - Add `files` to `SlackThreadStarter` type and extract from API response
  - Download thread starter files when the reply message has no attachments
  - Add verbose log for thread starter file hydration

  Fixes issue where asking about a PDF in a thread reply would fail because
  the model never received the file content from the root message.
This commit is contained in:
Travis Irby
2026-01-22 21:55:03 -05:00
committed by Peter Steinberger
parent 2accb47e4d
commit 578ac9f1a9
2 changed files with 23 additions and 4 deletions

View File

@@ -53,6 +53,7 @@ export type SlackThreadStarter = {
text: string; text: string;
userId?: string; userId?: string;
ts?: string; ts?: string;
files?: SlackFile[];
}; };
const THREAD_STARTER_CACHE = new Map<string, SlackThreadStarter>(); const THREAD_STARTER_CACHE = new Map<string, SlackThreadStarter>();
@@ -71,7 +72,7 @@ export async function resolveSlackThreadStarter(params: {
ts: params.threadTs, ts: params.threadTs,
limit: 1, limit: 1,
inclusive: true, inclusive: true,
})) as { messages?: Array<{ text?: string; user?: string; ts?: string }> }; })) as { messages?: Array<{ text?: string; user?: string; ts?: string; files?: SlackFile[] }> };
const message = response?.messages?.[0]; const message = response?.messages?.[0];
const text = (message?.text ?? "").trim(); const text = (message?.text ?? "").trim();
if (!message || !text) return null; if (!message || !text) return null;
@@ -79,6 +80,7 @@ export async function resolveSlackThreadStarter(params: {
text, text,
userId: message.user, userId: message.user,
ts: message.ts, ts: message.ts,
files: message.files,
}; };
THREAD_STARTER_CACHE.set(cacheKey, starter); THREAD_STARTER_CACHE.set(cacheKey, starter);
return starter; return starter;

View File

@@ -434,6 +434,7 @@ export async function prepareSlackMessage(params: {
let threadStarterBody: string | undefined; let threadStarterBody: string | undefined;
let threadLabel: string | undefined; let threadLabel: string | undefined;
let threadStarterMedia: Awaited<ReturnType<typeof resolveSlackMedia>> = null;
if (isThreadReply && threadTs) { if (isThreadReply && threadTs) {
const starter = await resolveSlackThreadStarter({ const starter = await resolveSlackThreadStarter({
channelId: message.channel, channelId: message.channel,
@@ -453,11 +454,27 @@ export async function prepareSlackMessage(params: {
}); });
const snippet = starter.text.replace(/\s+/g, " ").slice(0, 80); const snippet = starter.text.replace(/\s+/g, " ").slice(0, 80);
threadLabel = `Slack thread ${roomLabel}${snippet ? `: ${snippet}` : ""}`; threadLabel = `Slack thread ${roomLabel}${snippet ? `: ${snippet}` : ""}`;
// If current message has no files but thread starter does, fetch starter's files
if (!media && starter.files && starter.files.length > 0) {
threadStarterMedia = await resolveSlackMedia({
files: starter.files,
token: ctx.botToken,
maxBytes: ctx.mediaMaxBytes,
});
if (threadStarterMedia) {
logVerbose(
`slack: hydrated thread starter file ${threadStarterMedia.placeholder} from root message`,
);
}
}
} else { } else {
threadLabel = `Slack thread ${roomLabel}`; threadLabel = `Slack thread ${roomLabel}`;
} }
} }
// Use thread starter media if current message has none
const effectiveMedia = media ?? threadStarterMedia;
const ctxPayload = finalizeInboundContext({ const ctxPayload = finalizeInboundContext({
Body: combinedBody, Body: combinedBody,
RawBody: rawBody, RawBody: rawBody,
@@ -483,9 +500,9 @@ export async function prepareSlackMessage(params: {
ThreadLabel: threadLabel, ThreadLabel: threadLabel,
Timestamp: message.ts ? Math.round(Number(message.ts) * 1000) : undefined, Timestamp: message.ts ? Math.round(Number(message.ts) * 1000) : undefined,
WasMentioned: isRoomish ? effectiveWasMentioned : undefined, WasMentioned: isRoomish ? effectiveWasMentioned : undefined,
MediaPath: media?.path, MediaPath: effectiveMedia?.path,
MediaType: media?.contentType, MediaType: effectiveMedia?.contentType,
MediaUrl: media?.path, MediaUrl: effectiveMedia?.path,
CommandAuthorized: commandAuthorized, CommandAuthorized: commandAuthorized,
OriginatingChannel: "slack" as const, OriginatingChannel: "slack" as const,
OriginatingTo: slackTo, OriginatingTo: slackTo,