refactor: normalize inbound context
This commit is contained in:
@@ -120,12 +120,18 @@ export async function resolveReplyDirectives(params: {
|
||||
// Prefer CommandBody/RawBody (clean message without structural context) for directive parsing.
|
||||
// Keep `Body`/`BodyStripped` as the best-available prompt text (may include context).
|
||||
const commandSource =
|
||||
sessionCtx.BodyForCommands ??
|
||||
sessionCtx.CommandBody ??
|
||||
sessionCtx.RawBody ??
|
||||
sessionCtx.Transcript ??
|
||||
sessionCtx.BodyStripped ??
|
||||
sessionCtx.Body ??
|
||||
ctx.BodyForCommands ??
|
||||
ctx.CommandBody ??
|
||||
ctx.RawBody ??
|
||||
"";
|
||||
const promptSource = sessionCtx.BodyForAgent ?? sessionCtx.BodyStripped ?? sessionCtx.Body ?? "";
|
||||
const commandText = commandSource || promptSource;
|
||||
const command = buildCommandContext({
|
||||
ctx,
|
||||
cfg,
|
||||
@@ -162,7 +168,7 @@ export async function resolveReplyDirectives(params: {
|
||||
.filter((alias): alias is string => Boolean(alias))
|
||||
.filter((alias) => !reservedCommands.has(alias.toLowerCase()));
|
||||
const allowStatusDirective = allowTextCommands && command.isAuthorizedSender;
|
||||
let parsedDirectives = parseInlineDirectives(commandSource, {
|
||||
let parsedDirectives = parseInlineDirectives(commandText, {
|
||||
modelAliases: configuredAliases,
|
||||
allowStatusDirective,
|
||||
});
|
||||
@@ -253,6 +259,7 @@ export async function resolveReplyDirectives(params: {
|
||||
cleanedBody = stripInlineStatus(cleanedBody).cleaned;
|
||||
}
|
||||
|
||||
sessionCtx.BodyForAgent = cleanedBody;
|
||||
sessionCtx.Body = cleanedBody;
|
||||
sessionCtx.BodyStripped = cleanedBody;
|
||||
|
||||
@@ -402,7 +409,7 @@ export async function resolveReplyDirectives(params: {
|
||||
return {
|
||||
kind: "continue",
|
||||
result: {
|
||||
commandSource,
|
||||
commandSource: commandText,
|
||||
command,
|
||||
allowTextCommands,
|
||||
skillCommands,
|
||||
|
||||
@@ -135,7 +135,9 @@ export async function handleInlineActions(params: {
|
||||
].filter((entry): entry is string => Boolean(entry));
|
||||
const rewrittenBody = promptParts.join("\n\n");
|
||||
ctx.Body = rewrittenBody;
|
||||
ctx.BodyForAgent = rewrittenBody;
|
||||
sessionCtx.Body = rewrittenBody;
|
||||
sessionCtx.BodyForAgent = rewrittenBody;
|
||||
sessionCtx.BodyStripped = rewrittenBody;
|
||||
cleanedBody = rewrittenBody;
|
||||
}
|
||||
@@ -153,6 +155,7 @@ export async function handleInlineActions(params: {
|
||||
if (inlineCommand) {
|
||||
cleanedBody = inlineCommand.cleaned;
|
||||
sessionCtx.Body = cleanedBody;
|
||||
sessionCtx.BodyForAgent = cleanedBody;
|
||||
sessionCtx.BodyStripped = cleanedBody;
|
||||
}
|
||||
|
||||
|
||||
@@ -295,7 +295,10 @@ export async function runPreparedReply(
|
||||
abortKey: command.abortKey,
|
||||
messageId: sessionCtx.MessageSid,
|
||||
});
|
||||
const isGroupSession = sessionEntry?.chatType === "group" || sessionEntry?.chatType === "room";
|
||||
const isGroupSession =
|
||||
sessionEntry?.chatType === "group" ||
|
||||
sessionEntry?.chatType === "channel" ||
|
||||
sessionEntry?.chatType === "room";
|
||||
const isMainSession = !isGroupSession && sessionKey === normalizeMainKey(sessionCfg?.mainKey);
|
||||
prefixedBodyBase = await prependSystemEvents({
|
||||
cfg,
|
||||
|
||||
@@ -28,10 +28,10 @@ describe("formatInboundBodyWithSenderMeta", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("preserves escaped newline style when body uses literal \\\\n", () => {
|
||||
it("appends with a real newline even if the body contains literal \\\\n", () => {
|
||||
const ctx: MsgContext = { ChatType: "group", SenderName: "Bob", SenderId: "+222" };
|
||||
expect(formatInboundBodyWithSenderMeta({ ctx, body: "[X] one\\n[X] two" })).toBe(
|
||||
"[X] one\\n[X] two\\n[from: Bob (+222)]",
|
||||
"[X] one\\n[X] two\n[from: Bob (+222)]",
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,28 +1,21 @@
|
||||
import type { MsgContext } from "../templating.js";
|
||||
import { normalizeChatType } from "../../channels/chat-type.js";
|
||||
|
||||
export function formatInboundBodyWithSenderMeta(params: { body: string; ctx: MsgContext }): string {
|
||||
const body = params.body;
|
||||
if (!body.trim()) return body;
|
||||
const chatType = params.ctx.ChatType?.trim().toLowerCase();
|
||||
const chatType = normalizeChatType(params.ctx.ChatType);
|
||||
if (!chatType || chatType === "direct") return body;
|
||||
if (hasSenderMetaLine(body)) return body;
|
||||
|
||||
const senderLabel = formatSenderLabel(params.ctx);
|
||||
if (!senderLabel) return body;
|
||||
|
||||
const lineBreak = resolveBodyLineBreak(body);
|
||||
return `${body}${lineBreak}[from: ${senderLabel}]`;
|
||||
}
|
||||
|
||||
function resolveBodyLineBreak(body: string): string {
|
||||
const hasEscaped = body.includes("\\n");
|
||||
const hasNewline = body.includes("\n");
|
||||
if (hasEscaped && !hasNewline) return "\\n";
|
||||
return "\n";
|
||||
return `${body}\n[from: ${senderLabel}]`;
|
||||
}
|
||||
|
||||
function hasSenderMetaLine(body: string): boolean {
|
||||
return /(^|\n|\\n)\[from:/i.test(body);
|
||||
return /(^|\n)\[from:/i.test(body);
|
||||
}
|
||||
|
||||
function formatSenderLabel(ctx: MsgContext): string | null {
|
||||
|
||||
@@ -25,8 +25,10 @@ import {
|
||||
import { normalizeMainKey } from "../../routing/session-key.js";
|
||||
import { resolveCommandAuthorization } from "../command-auth.js";
|
||||
import type { MsgContext, TemplateContext } from "../templating.js";
|
||||
import { normalizeChatType } from "../../channels/chat-type.js";
|
||||
import { stripMentions, stripStructuralPrefixes } from "./mentions.js";
|
||||
import { formatInboundBodyWithSenderMeta } from "./inbound-sender-meta.js";
|
||||
import { normalizeInboundTextNewlines } from "./inbound-text.js";
|
||||
|
||||
export type SessionInitResult = {
|
||||
sessionCtx: TemplateContext;
|
||||
@@ -126,10 +128,11 @@ export async function initSessionState(params: {
|
||||
let persistedProviderOverride: string | undefined;
|
||||
|
||||
const groupResolution = resolveGroupSessionKey(sessionCtxForState) ?? undefined;
|
||||
const isGroup = ctx.ChatType?.trim().toLowerCase() === "group" || Boolean(groupResolution);
|
||||
const normalizedChatType = normalizeChatType(ctx.ChatType);
|
||||
const isGroup = normalizedChatType != null && normalizedChatType !== "direct" ? true : Boolean(groupResolution);
|
||||
// Prefer CommandBody/RawBody (clean message) for command detection; fall back
|
||||
// to Body which may contain structural context (history, sender labels).
|
||||
const commandSource = ctx.CommandBody ?? ctx.RawBody ?? ctx.Body ?? "";
|
||||
const commandSource = ctx.BodyForCommands ?? ctx.CommandBody ?? ctx.RawBody ?? ctx.Body ?? "";
|
||||
const triggerBodyNormalized = stripStructuralPrefixes(commandSource).trim().toLowerCase();
|
||||
|
||||
// Use CommandBody/RawBody for reset trigger matching (clean message without structural context).
|
||||
@@ -308,7 +311,15 @@ export async function initSessionState(params: {
|
||||
// RawBody is reserved for command/directive parsing and may omit context.
|
||||
BodyStripped: formatInboundBodyWithSenderMeta({
|
||||
ctx,
|
||||
body: bodyStripped ?? ctx.Body ?? ctx.CommandBody ?? ctx.RawBody ?? "",
|
||||
body: normalizeInboundTextNewlines(
|
||||
bodyStripped ??
|
||||
ctx.BodyForAgent ??
|
||||
ctx.Body ??
|
||||
ctx.CommandBody ??
|
||||
ctx.RawBody ??
|
||||
ctx.BodyForCommands ??
|
||||
"",
|
||||
),
|
||||
}),
|
||||
SessionId: sessionId,
|
||||
IsNewSession: isNewSession ? "true" : "false",
|
||||
|
||||
@@ -253,6 +253,7 @@ export function buildStatusMessage(args: StatusArgs): string {
|
||||
|
||||
const isGroupSession =
|
||||
entry?.chatType === "group" ||
|
||||
entry?.chatType === "channel" ||
|
||||
entry?.chatType === "room" ||
|
||||
Boolean(args.sessionKey?.includes(":group:")) ||
|
||||
Boolean(args.sessionKey?.includes(":channel:")) ||
|
||||
|
||||
@@ -8,6 +8,11 @@ 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;
|
||||
/**
|
||||
* Raw message body without structural context (history, sender labels).
|
||||
* Legacy alias for CommandBody. Falls back to Body if not set.
|
||||
@@ -17,6 +22,11 @@ export type MsgContext = {
|
||||
* 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;
|
||||
@@ -46,6 +56,8 @@ export type MsgContext = {
|
||||
Prompt?: string;
|
||||
MaxChars?: number;
|
||||
ChatType?: string;
|
||||
/** Human label for envelope headers (conversation label, not sender). */
|
||||
ConversationLabel?: string;
|
||||
GroupSubject?: string;
|
||||
GroupRoom?: string;
|
||||
GroupSpace?: string;
|
||||
|
||||
Reference in New Issue
Block a user