refactor: assemble channel contexts in core

This commit is contained in:
Peter Steinberger
2026-05-15 14:07:52 +01:00
parent 8cc1aee9d8
commit f74436dc81
2 changed files with 185 additions and 111 deletions

View File

@@ -1,11 +1,11 @@
import {
buildChannelTurnContext,
formatInboundEnvelope,
resolveEnvelopeFormatOptions,
} from "openclaw/plugin-sdk/channel-inbound";
import { resolveChannelContextVisibilityMode } from "openclaw/plugin-sdk/context-visibility-runtime";
import { resolvePinnedMainDmOwnerFromAllowlist } from "openclaw/plugin-sdk/conversation-runtime";
import { isDangerousNameMatchingEnabled } from "openclaw/plugin-sdk/dangerous-name-runtime";
import { finalizeInboundContext } from "openclaw/plugin-sdk/reply-dispatch-runtime";
import { createChannelHistoryWindow } from "openclaw/plugin-sdk/reply-history";
import { buildAgentSessionKey, resolveThreadSessionKeys } from "openclaw/plugin-sdk/routing";
import { danger, logVerbose, shouldLogVerbose } from "openclaw/plugin-sdk/runtime-env";
@@ -22,7 +22,6 @@ import {
} from "./inbound-context.js";
import type { DiscordMessagePreflightContext } from "./message-handler.preflight.js";
import {
buildDiscordMediaPayload,
resolveReferencedReplyMediaList,
resolveDiscordMessageText,
type DiscordMediaInfo,
@@ -263,7 +262,6 @@ export async function buildDiscordMessageProcessContext(params: {
parentSessionKey = undefined;
}
}
const mediaPayload = buildDiscordMediaPayload(mediaListForContext);
const preflightAudioIndex =
preflightAudioTranscript === undefined
? -1
@@ -329,59 +327,102 @@ export async function buildDiscordMessageProcessContext(params: {
sessionKey: effectiveSessionKey,
});
const ctxPayload = finalizeInboundContext({
Body: combinedBody,
BodyForAgent: preflightAudioTranscript ?? baseText ?? text,
InboundHistory: inboundHistory,
RawBody: preflightAudioTranscript ?? baseText,
CommandBody: preflightAudioTranscript ?? baseText,
...(preflightAudioTranscript !== undefined ? { Transcript: preflightAudioTranscript } : {}),
From: effectiveFrom,
To: effectiveTo,
SessionKey: effectiveSessionKey,
AccountId: route.accountId,
ChatType: isDirectMessage ? "direct" : "channel",
ConversationLabel: fromLabel,
SenderName: senderName,
SenderId: sender.id,
SenderUsername: senderUsername,
SenderTag: sender.tag,
GroupSubject: groupSubject,
GroupChannel: groupChannel,
MemberRoleIds: memberRoleIds,
UntrustedContext: untrustedContext,
GroupSystemPrompt: isGuildMessage ? groupSystemPrompt : undefined,
GroupSpace: isGuildMessage ? (guildInfo?.id ?? guildSlug) || undefined : undefined,
OwnerAllowFrom: ownerAllowFrom,
Provider: "discord" as const,
Surface: "discord" as const,
WasMentioned: ctx.effectiveWasMentioned,
MessageSid: canonicalMessageId ?? message.id,
...(canonicalMessageId && canonicalMessageId !== message.id
? { MessageSidFull: message.id }
: {}),
ReplyToId: filteredReplyContext?.id,
ReplyToBody: filteredReplyContext?.body,
ReplyToSender: filteredReplyContext?.sender,
ParentSessionKey: autoThreadContext?.ParentSessionKey ?? threadKeys.parentSessionKey,
ModelParentSessionKey:
autoThreadContext?.ModelParentSessionKey ?? modelParentSessionKey ?? undefined,
MessageThreadId: threadChannel?.id ?? autoThreadContext?.createdThreadId ?? undefined,
ThreadStarterBody: !effectivePreviousTimestamp ? threadStarterBody : undefined,
ThreadLabel: threadLabel,
Timestamp: resolveTimestampMs(message.timestamp),
...mediaPayload,
...(preflightAudioIndex >= 0 ? { MediaTranscribedIndexes: [preflightAudioIndex] } : {}),
CommandAuthorized: commandAuthorized,
CommandTurn: {
const ctxPayload = buildChannelTurnContext({
channel: "discord",
provider: "discord",
surface: "discord",
accountId: route.accountId,
messageId: canonicalMessageId ?? message.id,
messageIdFull: canonicalMessageId && canonicalMessageId !== message.id ? message.id : undefined,
timestamp: resolveTimestampMs(message.timestamp),
from: effectiveFrom,
sender: {
id: sender.id,
name: senderName,
username: senderUsername,
tag: sender.tag,
roles: memberRoleIds,
displayLabel: senderLabel,
},
conversation: {
kind: isDirectMessage ? "direct" : "channel",
id: messageChannelId,
label: fromLabel,
spaceId: isGuildMessage ? (guildInfo?.id ?? guildSlug) || undefined : undefined,
threadId: threadChannel?.id ?? autoThreadContext?.createdThreadId ?? undefined,
routePeer: {
kind: isDirectMessage ? "direct" : "channel",
id: isDirectMessage ? author.id : messageChannelId,
},
},
route: {
agentId: route.agentId,
accountId: route.accountId,
routeSessionKey: route.sessionKey,
dispatchSessionKey: effectiveSessionKey,
parentSessionKey: autoThreadContext?.ParentSessionKey ?? threadKeys.parentSessionKey,
modelParentSessionKey:
autoThreadContext?.ModelParentSessionKey ?? modelParentSessionKey ?? undefined,
},
reply: {
to: effectiveTo,
originatingTo,
},
message: {
body: combinedBody,
rawBody: preflightAudioTranscript ?? baseText,
bodyForAgent: preflightAudioTranscript ?? baseText ?? text,
commandBody: preflightAudioTranscript ?? baseText,
envelopeFrom: fromLabel,
inboundHistory,
},
access: {
mentions: {
canDetectMention: ctx.canDetectMention,
wasMentioned: ctx.effectiveWasMentioned,
hasAnyMention: ctx.hasAnyMention,
requireMention: ctx.shouldRequireMention,
effectiveWasMentioned: ctx.effectiveWasMentioned,
},
commands: {
authorized: commandAuthorized,
allowTextCommands: ctx.allowTextCommands,
useAccessGroups: false,
authorizers: [],
},
},
commandTurn: {
kind: "text-slash" as const,
source: "text" as const,
authorized: commandAuthorized,
body: preflightAudioTranscript ?? baseText,
},
CommandSource: "text" as const,
OriginatingChannel: "discord" as const,
OriginatingTo: originatingTo,
media: mediaListForContext.map((media, index) => ({
path: media.path,
contentType: media.contentType,
transcribed: index === preflightAudioIndex,
})),
supplemental: {
quote: filteredReplyContext
? {
id: filteredReplyContext.id,
body: filteredReplyContext.body,
sender: filteredReplyContext.sender,
}
: undefined,
thread: {
starterBody: !effectivePreviousTimestamp ? threadStarterBody : undefined,
label: threadLabel,
},
groupSystemPrompt: isGuildMessage ? groupSystemPrompt : undefined,
},
extra: {
...(preflightAudioTranscript !== undefined ? { Transcript: preflightAudioTranscript } : {}),
GroupSubject: groupSubject,
GroupChannel: groupChannel,
UntrustedContext: untrustedContext,
OwnerAllowFrom: ownerAllowFrom,
},
});
const persistedSessionKey = ctxPayload.SessionKey ?? route.sessionKey;

View File

@@ -4,6 +4,7 @@ import {
type AckReactionScope,
} from "openclaw/plugin-sdk/channel-feedback";
import {
buildChannelTurnContext,
buildMentionRegexes,
formatInboundEnvelope,
implicitMentionKindWhen,
@@ -18,7 +19,6 @@ import { ensureConfiguredBindingRouteReady } from "openclaw/plugin-sdk/conversat
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import { recordDroppedChannelTurnHistory } from "openclaw/plugin-sdk/inbound-reply-dispatch";
import { mimeTypeFromFilePath } from "openclaw/plugin-sdk/media-mime";
import { finalizeInboundContext } from "openclaw/plugin-sdk/reply-dispatch-runtime";
import { createChannelHistoryWindow } from "openclaw/plugin-sdk/reply-history";
import type { FinalizedMsgContext } from "openclaw/plugin-sdk/reply-runtime";
import { resolveInboundLastRouteSessionKey } from "openclaw/plugin-sdk/routing";
@@ -1056,8 +1056,6 @@ export async function prepareSlackMessage(params: {
// Use direct media (including forwarded attachment media) if available, else thread starter media
const effectiveMedia = effectiveDirectMedia ?? threadStarterMedia;
const firstMedia = effectiveMedia?.[0];
const inboundHistory =
isRoomish && ctx.historyLimit > 0
? channelHistory.buildInboundHistory({
@@ -1067,63 +1065,98 @@ export async function prepareSlackMessage(params: {
: dmHistoryContext.inboundHistory;
const commandBody = textForCommandDetection.trim();
const ctxPayload = finalizeInboundContext({
Body: combinedBody,
BodyForAgent: rawBody,
InboundHistory: inboundHistory,
RawBody: rawBody,
CommandBody: commandBody,
BodyForCommands: commandBody,
From: slackFrom,
To: slackTo,
SessionKey: sessionKey,
AccountId: route.accountId,
ChatType: chatType,
ConversationLabel: envelopeFrom,
GroupSubject: isRoomish ? roomLabel : undefined,
GroupSpace: ctx.teamId || undefined,
GroupSystemPrompt: groupSystemPrompt,
UntrustedContext: untrustedChannelMetadata ? [untrustedChannelMetadata] : undefined,
SenderName: senderName,
SenderId: senderId,
Provider: "slack" as const,
Surface: "slack" as const,
MessageSid: message.ts,
ReplyToId: threadContext.replyToId,
// Preserve thread context for routed tool notifications.
MessageThreadId: threadContext.messageThreadId,
ParentSessionKey: threadKeys.parentSessionKey,
// Only include thread starter body for NEW sessions (existing sessions already have it in their transcript)
ThreadStarterBody: !threadSessionPreviousTimestamp ? threadStarterBody : undefined,
ThreadHistoryBody: threadHistoryBody,
IsFirstThreadTurn:
isThreadReply && threadTs && !threadSessionPreviousTimestamp ? true : undefined,
ThreadLabel: threadLabel,
Timestamp: message.ts ? Math.round(Number(message.ts) * 1000) : undefined,
...buildSlackMentionContextPayload({
isRoomish,
effectiveWasMentioned,
explicitlyMentioned,
mentionedUserIds,
mentionedSubteamIds,
matchedImplicitMentionKinds,
mentionSource,
}),
MediaPath: firstMedia?.path,
MediaType: firstMedia?.contentType,
MediaUrl: firstMedia?.path,
MediaPaths:
effectiveMedia && effectiveMedia.length > 0 ? effectiveMedia.map((m) => m.path) : undefined,
MediaUrls:
effectiveMedia && effectiveMedia.length > 0 ? effectiveMedia.map((m) => m.path) : undefined,
MediaTypes:
effectiveMedia && effectiveMedia.length > 0
? effectiveMedia.map((m) => m.contentType ?? "")
: undefined,
CommandAuthorized: commandAuthorized,
OriginatingChannel: "slack" as const,
OriginatingTo: slackTo,
NativeChannelId: message.channel,
const ctxPayload = buildChannelTurnContext({
channel: "slack",
provider: "slack",
surface: "slack",
accountId: route.accountId,
messageId: message.ts,
timestamp: message.ts ? Math.round(Number(message.ts) * 1000) : undefined,
from: slackFrom,
sender: {
id: senderId,
name: senderName,
displayLabel: senderName,
},
conversation: {
kind: chatType,
id: message.channel,
label: envelopeFrom,
spaceId: ctx.teamId || undefined,
threadId: threadContext.messageThreadId,
nativeChannelId: message.channel,
routePeer: {
kind: chatType,
id: message.channel,
},
},
route: {
agentId: route.agentId,
accountId: route.accountId,
routeSessionKey: sessionKey,
parentSessionKey: threadKeys.parentSessionKey,
},
reply: {
to: slackTo,
originatingTo: slackTo,
replyToId: threadContext.replyToId,
messageThreadId: threadContext.messageThreadId,
nativeChannelId: message.channel,
},
message: {
body: combinedBody,
bodyForAgent: rawBody,
rawBody,
commandBody,
envelopeFrom,
inboundHistory,
},
access: {
mentions: {
canDetectMention: isRoomish,
wasMentioned: effectiveWasMentioned,
hasAnyMention: explicitlyMentioned || mentionedSubteamIds.length > 0,
implicitMentionKinds: matchedImplicitMentionKinds as Array<
"reply_to_bot" | "quoted_bot" | "bot_thread_participant" | "native"
>,
requireMention: shouldRequireMention,
effectiveWasMentioned,
},
commands: {
authorized: commandAuthorized,
allowTextCommands,
useAccessGroups: false,
authorizers: [],
},
},
media: effectiveMedia?.map((media) => ({
path: media.path,
contentType: media.contentType,
})),
supplemental: {
thread: {
// Only include thread starter body for NEW sessions (existing sessions already have it in their transcript)
starterBody: !threadSessionPreviousTimestamp ? threadStarterBody : undefined,
historyBody: threadHistoryBody,
label: threadLabel,
},
groupSystemPrompt,
},
extra: {
GroupSubject: isRoomish ? roomLabel : undefined,
UntrustedContext: untrustedChannelMetadata ? [untrustedChannelMetadata] : undefined,
IsFirstThreadTurn:
isThreadReply && threadTs && !threadSessionPreviousTimestamp ? true : undefined,
...buildSlackMentionContextPayload({
isRoomish,
effectiveWasMentioned,
explicitlyMentioned,
mentionedUserIds,
mentionedSubteamIds,
matchedImplicitMentionKinds,
mentionSource,
}),
},
}) satisfies FinalizedMsgContext;
if (isRoomish && !shouldRequireMention) {