* feat(slack): populate thread session with existing thread history When a new session is created for a Slack thread, fetch and inject the full thread history as context. This preserves conversation continuity so the bot knows what it previously said in the thread. - Add resolveSlackThreadHistory() to fetch all thread messages - Add ThreadHistoryBody to context payload - Use thread history instead of just thread starter for new sessions Fixes #4470 * chore: remove redundant comments * fix: use threadContextNote in queue body * fix(slack): address Greptile review feedback - P0: Use thread session key (not base session key) for new-session check This ensures thread history is injected when the thread session is new, even if the base channel session already exists. - P1: Fetch up to 200 messages and take the most recent N Slack API returns messages in chronological order (oldest first). Previously we took the first N, now we take the last N for relevant context. - P1: Batch resolve user names with Promise.all Avoid N sequential API calls when resolving user names in thread history. - P2: Include file-only messages in thread history Messages with attachments but no text are now included with a placeholder like '[attached: image.png, document.pdf]'. - P2: Add documentation about intentional 200-message fetch limit Clarifies that we intentionally don't paginate; 200 covers most threads. * style: add braces for curly lint rule * feat(slack): add thread.initialHistoryLimit config option Allow users to configure the maximum number of thread messages to fetch when starting a new thread session. Defaults to 20. Set to 0 to disable thread history fetching entirely. This addresses the optional configuration request from #2608. * chore: trigger CI * fix(slack): ensure isNewSession=true on first thread turn recordInboundSession() in prepare.ts creates the thread session entry before session.ts reads the store, causing isNewSession to be false on the very first user message in a thread. This prevented thread context (history/starter) from being injected. Add IsFirstThreadTurn flag to message context, set when readSessionUpdatedAt() returns undefined for the thread session key. session.ts uses this flag to force isNewSession=true. * style: format prepare.ts for oxfmt * fix: suppress InboundHistory/ThreadStarterBody when ThreadHistoryBody present (#13912) When ThreadHistoryBody is fetched from the Slack API (conversations.replies), it already contains pending messages and the thread starter. Passing both InboundHistory and ThreadStarterBody alongside ThreadHistoryBody caused duplicate content in the LLM context on new thread sessions. Suppress InboundHistory and ThreadStarterBody when ThreadHistoryBody is present, since it is a strict superset of both. * remove verbose comment * fix(slack): paginate thread history context fetch * fix(slack): wire session file path options after main merge --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
205 lines
6.4 KiB
TypeScript
205 lines
6.4 KiB
TypeScript
import type { ChannelId } from "../channels/plugins/types.js";
|
|
import type {
|
|
MediaUnderstandingDecision,
|
|
MediaUnderstandingOutput,
|
|
} from "../media-understanding/types.js";
|
|
import type { StickerMetadata } from "../telegram/bot/types.js";
|
|
import type { InternalMessageChannel } from "../utils/message-channel.js";
|
|
import type { CommandArgs } from "./commands-registry.types.js";
|
|
|
|
/** Valid message channels for routing. */
|
|
export type OriginatingChannelType = ChannelId | InternalMessageChannel;
|
|
|
|
export type MsgContext = {
|
|
Body?: string;
|
|
/**
|
|
* Agent prompt body (may include envelope/history/context). Prefer this for prompt shaping.
|
|
* Should use real newlines (`\n`), not escaped `\\n`.
|
|
*/
|
|
BodyForAgent?: string;
|
|
/**
|
|
* Recent chat history for context (untrusted user content). Prefer passing this
|
|
* as structured context blocks in the user prompt rather than rendering plaintext envelopes.
|
|
*/
|
|
InboundHistory?: Array<{
|
|
sender: string;
|
|
body: string;
|
|
timestamp?: number;
|
|
}>;
|
|
/**
|
|
* Raw message body without structural context (history, sender labels).
|
|
* Legacy alias for CommandBody. Falls back to Body if not set.
|
|
*/
|
|
RawBody?: string;
|
|
/**
|
|
* Prefer for command detection; RawBody is treated as legacy alias.
|
|
*/
|
|
CommandBody?: string;
|
|
/**
|
|
* Command parsing body. Prefer this over CommandBody/RawBody when set.
|
|
* Should be the "clean" text (no history/sender context).
|
|
*/
|
|
BodyForCommands?: string;
|
|
CommandArgs?: CommandArgs;
|
|
From?: string;
|
|
To?: string;
|
|
SessionKey?: string;
|
|
/** Provider account id (multi-account). */
|
|
AccountId?: string;
|
|
ParentSessionKey?: string;
|
|
MessageSid?: string;
|
|
/** Provider-specific full message id when MessageSid is a shortened alias. */
|
|
MessageSidFull?: string;
|
|
MessageSids?: string[];
|
|
MessageSidFirst?: string;
|
|
MessageSidLast?: string;
|
|
ReplyToId?: string;
|
|
/** Provider-specific full reply-to id when ReplyToId is a shortened alias. */
|
|
ReplyToIdFull?: string;
|
|
ReplyToBody?: string;
|
|
ReplyToSender?: string;
|
|
ReplyToIsQuote?: boolean;
|
|
ForwardedFrom?: string;
|
|
ForwardedFromType?: string;
|
|
ForwardedFromId?: string;
|
|
ForwardedFromUsername?: string;
|
|
ForwardedFromTitle?: string;
|
|
ForwardedFromSignature?: string;
|
|
ForwardedFromChatType?: string;
|
|
ForwardedFromMessageId?: number;
|
|
ForwardedDate?: number;
|
|
ThreadStarterBody?: string;
|
|
/** Full thread history when starting a new thread session. */
|
|
ThreadHistoryBody?: string;
|
|
IsFirstThreadTurn?: boolean;
|
|
ThreadLabel?: string;
|
|
MediaPath?: string;
|
|
MediaUrl?: string;
|
|
MediaType?: string;
|
|
MediaDir?: string;
|
|
MediaPaths?: string[];
|
|
MediaUrls?: string[];
|
|
MediaTypes?: string[];
|
|
/** Telegram sticker metadata (emoji, set name, file IDs, cached description). */
|
|
Sticker?: StickerMetadata;
|
|
OutputDir?: string;
|
|
OutputBase?: string;
|
|
/** Remote host for SCP when media lives on a different machine (e.g., openclaw@192.168.64.3). */
|
|
MediaRemoteHost?: string;
|
|
Transcript?: string;
|
|
MediaUnderstanding?: MediaUnderstandingOutput[];
|
|
MediaUnderstandingDecisions?: MediaUnderstandingDecision[];
|
|
LinkUnderstanding?: string[];
|
|
Prompt?: string;
|
|
MaxChars?: number;
|
|
ChatType?: string;
|
|
/** Human label for envelope headers (conversation label, not sender). */
|
|
ConversationLabel?: string;
|
|
GroupSubject?: string;
|
|
/** Human label for channel-like group conversations (e.g. #general, #support). */
|
|
GroupChannel?: string;
|
|
GroupSpace?: string;
|
|
GroupMembers?: string;
|
|
GroupSystemPrompt?: string;
|
|
/** Untrusted metadata that must not be treated as system instructions. */
|
|
UntrustedContext?: string[];
|
|
/** Explicit owner allowlist overrides (trusted, configuration-derived). */
|
|
OwnerAllowFrom?: Array<string | number>;
|
|
SenderName?: string;
|
|
SenderId?: string;
|
|
SenderUsername?: string;
|
|
SenderTag?: string;
|
|
SenderE164?: string;
|
|
Timestamp?: number;
|
|
/** Provider label (e.g. whatsapp, telegram). */
|
|
Provider?: string;
|
|
/** Provider surface label (e.g. discord, slack). Prefer this over `Provider` when available. */
|
|
Surface?: string;
|
|
WasMentioned?: boolean;
|
|
CommandAuthorized?: boolean;
|
|
CommandSource?: "text" | "native";
|
|
CommandTargetSessionKey?: string;
|
|
/** Gateway client scopes when the message originates from the gateway. */
|
|
GatewayClientScopes?: string[];
|
|
/** Thread identifier (Telegram topic id or Matrix thread event id). */
|
|
MessageThreadId?: string | number;
|
|
/** Telegram forum supergroup marker. */
|
|
IsForum?: boolean;
|
|
/**
|
|
* Originating channel for reply routing.
|
|
* When set, replies should be routed back to this provider
|
|
* instead of using lastChannel from the session.
|
|
*/
|
|
OriginatingChannel?: OriginatingChannelType;
|
|
/**
|
|
* Originating destination for reply routing.
|
|
* The chat/channel/user ID where the reply should be sent.
|
|
*/
|
|
OriginatingTo?: string;
|
|
/**
|
|
* Messages from hooks to be included in the response.
|
|
* Used for hook confirmation messages like "Session context saved to memory".
|
|
*/
|
|
HookMessages?: string[];
|
|
};
|
|
|
|
export type FinalizedMsgContext = Omit<MsgContext, "CommandAuthorized"> & {
|
|
/**
|
|
* Always set by finalizeInboundContext().
|
|
* Default-deny: missing/undefined becomes false.
|
|
*/
|
|
CommandAuthorized: boolean;
|
|
};
|
|
|
|
export type TemplateContext = MsgContext & {
|
|
BodyStripped?: string;
|
|
SessionId?: string;
|
|
IsNewSession?: string;
|
|
};
|
|
|
|
function formatTemplateValue(value: unknown): string {
|
|
if (value == null) {
|
|
return "";
|
|
}
|
|
if (typeof value === "string") {
|
|
return value;
|
|
}
|
|
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
|
|
return String(value);
|
|
}
|
|
if (typeof value === "symbol" || typeof value === "function") {
|
|
return value.toString();
|
|
}
|
|
if (Array.isArray(value)) {
|
|
return value
|
|
.flatMap((entry) => {
|
|
if (entry == null) {
|
|
return [];
|
|
}
|
|
if (typeof entry === "string") {
|
|
return [entry];
|
|
}
|
|
if (typeof entry === "number" || typeof entry === "boolean" || typeof entry === "bigint") {
|
|
return [String(entry)];
|
|
}
|
|
return [];
|
|
})
|
|
.join(",");
|
|
}
|
|
if (typeof value === "object") {
|
|
return "";
|
|
}
|
|
return "";
|
|
}
|
|
|
|
// Simple {{Placeholder}} interpolation using inbound message context.
|
|
export function applyTemplate(str: string | undefined, ctx: TemplateContext) {
|
|
if (!str) {
|
|
return "";
|
|
}
|
|
return str.replace(/{{\s*(\w+)\s*}}/g, (_, key) => {
|
|
const value = ctx[key as keyof TemplateContext];
|
|
return formatTemplateValue(value);
|
|
});
|
|
}
|