This commit is contained in:
Masataka Shinohara 2026-01-30 17:05:32 +05:30 committed by GitHub
commit 064223b07e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 113 additions and 7 deletions

View File

@ -227,10 +227,13 @@ export async function runPreparedReply(
prefixedBodyBase, prefixedBodyBase,
}); });
const threadStarterBody = ctx.ThreadStarterBody?.trim(); const threadStarterBody = ctx.ThreadStarterBody?.trim();
const threadStarterNote = const threadHistoryBody = ctx.ThreadHistoryBody?.trim();
isNewSession && threadStarterBody const threadContextNote =
? `[Thread starter - for context]\n${threadStarterBody}` isNewSession && threadHistoryBody
: undefined; ? `[Thread history - for context]\n${threadHistoryBody}`
: isNewSession && threadStarterBody
? `[Thread starter - for context]\n${threadStarterBody}`
: undefined;
const skillResult = await ensureSkillSnapshot({ const skillResult = await ensureSkillSnapshot({
sessionEntry, sessionEntry,
sessionStore, sessionStore,
@ -245,7 +248,7 @@ export async function runPreparedReply(
sessionEntry = skillResult.sessionEntry ?? sessionEntry; sessionEntry = skillResult.sessionEntry ?? sessionEntry;
currentSystemSent = skillResult.systemSent; currentSystemSent = skillResult.systemSent;
const skillsSnapshot = skillResult.skillsSnapshot; const skillsSnapshot = skillResult.skillsSnapshot;
const prefixedBody = [threadStarterNote, prefixedBodyBase].filter(Boolean).join("\n\n"); const prefixedBody = [threadContextNote, prefixedBodyBase].filter(Boolean).join("\n\n");
const mediaNote = buildInboundMediaNote(ctx); const mediaNote = buildInboundMediaNote(ctx);
const mediaReplyHint = mediaNote const mediaReplyHint = mediaNote
? "To send an image back, prefer the message tool (media/path/filePath). If you must inline, use MEDIA:/path or MEDIA:https://example.com/image.jpg (spaces ok, quote if needed). Keep caption in the text body." ? "To send an image back, prefer the message tool (media/path/filePath). If you must inline, use MEDIA:/path or MEDIA:https://example.com/image.jpg (spaces ok, quote if needed). Keep caption in the text body."
@ -307,7 +310,7 @@ export async function runPreparedReply(
} }
const sessionIdFinal = sessionId ?? crypto.randomUUID(); const sessionIdFinal = sessionId ?? crypto.randomUUID();
const sessionFile = resolveSessionFilePath(sessionIdFinal, sessionEntry); const sessionFile = resolveSessionFilePath(sessionIdFinal, sessionEntry);
const queueBodyBase = [threadStarterNote, baseBodyFinal].filter(Boolean).join("\n\n"); const queueBodyBase = [threadContextNote, baseBodyFinal].filter(Boolean).join("\n\n");
const queueMessageId = sessionCtx.MessageSid?.trim(); const queueMessageId = sessionCtx.MessageSid?.trim();
const queueMessageIdHint = queueMessageId ? `[message_id: ${queueMessageId}]` : ""; const queueMessageIdHint = queueMessageId ? `[message_id: ${queueMessageId}]` : "";
const queueBodyWithId = queueMessageIdHint const queueBodyWithId = queueMessageIdHint

View File

@ -29,6 +29,7 @@ export function finalizeInboundContext<T extends Record<string, unknown>>(
normalized.CommandBody = normalizeTextField(normalized.CommandBody); normalized.CommandBody = normalizeTextField(normalized.CommandBody);
normalized.Transcript = normalizeTextField(normalized.Transcript); normalized.Transcript = normalizeTextField(normalized.Transcript);
normalized.ThreadStarterBody = normalizeTextField(normalized.ThreadStarterBody); normalized.ThreadStarterBody = normalizeTextField(normalized.ThreadStarterBody);
normalized.ThreadHistoryBody = normalizeTextField(normalized.ThreadHistoryBody);
const chatType = normalizeChatType(normalized.ChatType); const chatType = normalizeChatType(normalized.ChatType);
if (chatType && (opts.forceChatType || normalized.ChatType !== chatType)) { if (chatType && (opts.forceChatType || normalized.ChatType !== chatType)) {

View File

@ -58,6 +58,8 @@ export type MsgContext = {
ForwardedFromSignature?: string; ForwardedFromSignature?: string;
ForwardedDate?: number; ForwardedDate?: number;
ThreadStarterBody?: string; ThreadStarterBody?: string;
/** Full thread history when starting a new thread session. */
ThreadHistoryBody?: string;
ThreadLabel?: string; ThreadLabel?: string;
MediaPath?: string; MediaPath?: string;
MediaUrl?: string; MediaUrl?: string;

View File

@ -122,3 +122,59 @@ export async function resolveSlackThreadStarter(params: {
return null; return null;
} }
} }
export type SlackThreadMessage = {
text: string;
userId?: string;
ts?: string;
botId?: string;
files?: SlackFile[];
};
/**
* Fetches all messages in a Slack thread (excluding the current message).
* Used to populate thread context when a new thread session starts.
*/
export async function resolveSlackThreadHistory(params: {
channelId: string;
threadTs: string;
client: SlackWebClient;
currentMessageTs?: string;
limit?: number;
}): Promise<SlackThreadMessage[]> {
const maxMessages = params.limit ?? 20;
try {
const response = (await params.client.conversations.replies({
channel: params.channelId,
ts: params.threadTs,
limit: maxMessages + 1, // +1 to account for filtering current message
inclusive: true,
})) as {
messages?: Array<{
text?: string;
user?: string;
bot_id?: string;
ts?: string;
files?: SlackFile[];
}>;
};
const messages = response?.messages ?? [];
return messages
.filter((msg) => {
if (!msg.text?.trim()) return false;
if (params.currentMessageTs && msg.ts === params.currentMessageTs) return false;
return true;
})
.slice(0, maxMessages)
.map((msg) => ({
text: msg.text ?? "",
userId: msg.user,
botId: msg.bot_id,
ts: msg.ts,
files: msg.files,
}));
} catch {
return [];
}
}

View File

@ -44,7 +44,11 @@ import { resolveSlackAllowListMatch, resolveSlackUserAllowed } from "../allow-li
import { resolveSlackEffectiveAllowFrom } from "../auth.js"; import { resolveSlackEffectiveAllowFrom } from "../auth.js";
import { resolveSlackChannelConfig } from "../channel-config.js"; import { resolveSlackChannelConfig } from "../channel-config.js";
import { normalizeSlackChannelType, type SlackMonitorContext } from "../context.js"; import { normalizeSlackChannelType, type SlackMonitorContext } from "../context.js";
import { resolveSlackMedia, resolveSlackThreadStarter } from "../media.js"; import {
resolveSlackMedia,
resolveSlackThreadHistory,
resolveSlackThreadStarter,
} from "../media.js";
import type { PreparedSlackMessage } from "./types.js"; import type { PreparedSlackMessage } from "./types.js";
@ -452,6 +456,7 @@ export async function prepareSlackMessage(params: {
systemPromptParts.length > 0 ? systemPromptParts.join("\n\n") : undefined; systemPromptParts.length > 0 ? systemPromptParts.join("\n\n") : undefined;
let threadStarterBody: string | undefined; let threadStarterBody: string | undefined;
let threadHistoryBody: string | undefined;
let threadLabel: string | undefined; let threadLabel: string | undefined;
let threadStarterMedia: Awaited<ReturnType<typeof resolveSlackMedia>> = null; let threadStarterMedia: Awaited<ReturnType<typeof resolveSlackMedia>> = null;
if (isThreadReply && threadTs) { if (isThreadReply && threadTs) {
@ -489,6 +494,44 @@ export async function prepareSlackMessage(params: {
} else { } else {
threadLabel = `Slack thread ${roomLabel}`; threadLabel = `Slack thread ${roomLabel}`;
} }
// Fetch full thread history for new thread sessions
// This provides context of previous messages (including bot replies) in the thread
if (!previousTimestamp) {
const threadHistory = await resolveSlackThreadHistory({
channelId: message.channel,
threadTs,
client: ctx.app.client,
currentMessageTs: message.ts,
limit: 20,
});
if (threadHistory.length > 0) {
const historyParts: string[] = [];
for (const historyMsg of threadHistory) {
const msgUser = historyMsg.userId ? await ctx.resolveUserName(historyMsg.userId) : null;
const msgSenderName =
msgUser?.name ?? (historyMsg.botId ? `Bot (${historyMsg.botId})` : "Unknown");
const isBot = Boolean(historyMsg.botId);
const role = isBot ? "assistant" : "user";
const msgWithId = `${historyMsg.text}\n[slack message id: ${historyMsg.ts ?? "unknown"} channel: ${message.channel}]`;
historyParts.push(
formatInboundEnvelope({
channel: "Slack",
from: `${msgSenderName} (${role})`,
timestamp: historyMsg.ts ? Math.round(Number(historyMsg.ts) * 1000) : undefined,
body: msgWithId,
chatType: "channel",
envelope: envelopeOptions,
}),
);
}
threadHistoryBody = historyParts.join("\n\n");
logVerbose(
`slack: populated thread history with ${threadHistory.length} messages for new session`,
);
}
}
} }
// Use thread starter media if current message has none // Use thread starter media if current message has none
@ -516,6 +559,7 @@ export async function prepareSlackMessage(params: {
MessageThreadId: threadContext.messageThreadId, MessageThreadId: threadContext.messageThreadId,
ParentSessionKey: threadKeys.parentSessionKey, ParentSessionKey: threadKeys.parentSessionKey,
ThreadStarterBody: threadStarterBody, ThreadStarterBody: threadStarterBody,
ThreadHistoryBody: threadHistoryBody,
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,