fix: scope history injection to pending-only

This commit is contained in:
Peter Steinberger
2026-01-16 23:52:14 +00:00
parent 56ed5cc2d9
commit e31251293b
21 changed files with 278 additions and 175 deletions

View File

@@ -264,7 +264,7 @@ describe("monitorSlackProvider tool results", () => {
expect(sendMock.mock.calls[1][1]).toBe("final reply");
});
it("wraps room history in Body and preserves RawBody", async () => {
it("preserves RawBody without injecting processed room history", async () => {
config = {
messages: { ackReactionScope: "group-mentions" },
channels: {
@@ -320,9 +320,9 @@ describe("monitorSlackProvider tool results", () => {
await run;
expect(replyMock).toHaveBeenCalledTimes(2);
expect(capturedCtx.Body).toContain(HISTORY_CONTEXT_MARKER);
expect(capturedCtx.Body).toContain(CURRENT_MESSAGE_MARKER);
expect(capturedCtx.Body).toContain("first");
expect(capturedCtx.Body).not.toContain(HISTORY_CONTEXT_MARKER);
expect(capturedCtx.Body).not.toContain(CURRENT_MESSAGE_MARKER);
expect(capturedCtx.Body).not.toContain("first");
expect(capturedCtx.RawBody).toBe("second");
expect(capturedCtx.CommandBody).toBe("second");
});
@@ -334,7 +334,7 @@ describe("monitorSlackProvider tool results", () => {
slack: {
historyLimit: 5,
dm: { enabled: true, policy: "open", allowFrom: ["*"] },
channels: { C1: { allow: true, requireMention: false } },
channels: { C1: { allow: true, requireMention: true } },
},
},
};
@@ -372,7 +372,7 @@ describe("monitorSlackProvider tool results", () => {
event: {
type: "message",
user: "U1",
text: "thread-a-two",
text: "<@bot-user> thread-a-two",
ts: "201",
thread_ts: "100",
channel: "C1",
@@ -384,7 +384,7 @@ describe("monitorSlackProvider tool results", () => {
event: {
type: "message",
user: "U2",
text: "thread-b-one",
text: "<@bot-user> thread-b-one",
ts: "301",
thread_ts: "300",
channel: "C1",
@@ -396,10 +396,10 @@ describe("monitorSlackProvider tool results", () => {
controller.abort();
await run;
expect(replyMock).toHaveBeenCalledTimes(3);
expect(capturedCtx[1]?.Body).toContain("thread-a-one");
expect(capturedCtx[2]?.Body).not.toContain("thread-a-one");
expect(capturedCtx[2]?.Body).not.toContain("thread-a-two");
expect(replyMock).toHaveBeenCalledTimes(2);
expect(capturedCtx[0]?.Body).toContain("thread-a-one");
expect(capturedCtx[1]?.Body).not.toContain("thread-a-one");
expect(capturedCtx[1]?.Body).not.toContain("thread-a-two");
});
it("updates assistant thread status when replies start", async () => {

View File

@@ -66,8 +66,6 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
});
};
let didSendReply = false;
// Create mutable context for response prefix template interpolation
let prefixContext: ResponsePrefixContext = {
identityName: resolveIdentityName(cfg, route.agentId),
@@ -88,7 +86,6 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
textLimit: ctx.textLimit,
replyThreadTs,
});
didSendReply = true;
replyPlan.markSent();
},
onError: (err, info) => {
@@ -136,7 +133,7 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
}
if (!queuedFinal) {
if (prepared.isRoomish && ctx.historyLimit > 0 && didSendReply) {
if (prepared.isRoomish && ctx.historyLimit > 0) {
clearHistoryEntries({
historyMap: ctx.channelHistories,
historyKey: prepared.historyKey,
@@ -168,7 +165,7 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
});
}
if (prepared.isRoomish && ctx.historyLimit > 0 && didSendReply) {
if (prepared.isRoomish && ctx.historyLimit > 0) {
clearHistoryEntries({
historyMap: ctx.channelHistories,
historyKey: prepared.historyKey,

View File

@@ -2,7 +2,10 @@ import { resolveAckReaction } from "../../../agents/identity.js";
import { hasControlCommand } from "../../../auto-reply/command-detection.js";
import { shouldHandleTextCommands } from "../../../auto-reply/commands-registry.js";
import { formatAgentEnvelope, formatThreadStarterEnvelope } from "../../../auto-reply/envelope.js";
import { buildHistoryContextFromMap } from "../../../auto-reply/reply/history.js";
import {
buildPendingHistoryContextFromMap,
recordPendingHistoryEntry,
} from "../../../auto-reply/reply/history.js";
import { buildMentionRegexes, matchesMentionPatterns } from "../../../auto-reply/reply/mentions.js";
import { logVerbose, shouldLogVerbose } from "../../../globals.js";
import { enqueueSystemEvent } from "../../../infra/system-events.js";
@@ -167,6 +170,19 @@ export async function prepareSlackMessage(params: {
},
});
const baseSessionKey = route.sessionKey;
const threadTs = message.thread_ts;
const hasThreadTs = typeof threadTs === "string" && threadTs.length > 0;
const isThreadReply = hasThreadTs && (threadTs !== message.ts || Boolean(message.parent_user_id));
const threadKeys = resolveThreadSessionKeys({
baseSessionKey,
threadId: isThreadReply ? threadTs : undefined,
parentSessionKey: isThreadReply && ctx.threadInheritParent ? baseSessionKey : undefined,
});
const sessionKey = threadKeys.sessionKey;
const historyKey =
isThreadReply && ctx.threadHistoryScope === "thread" ? sessionKey : message.channel;
const mentionRegexes = buildMentionRegexes(cfg, route.agentId);
const wasMentioned =
opts.wasMentioned ??
@@ -233,6 +249,28 @@ export async function prepareSlackMessage(params: {
const effectiveWasMentioned = mentionGate.effectiveWasMentioned;
if (isRoom && shouldRequireMention && mentionGate.shouldSkip) {
ctx.logger.info({ channel: message.channel, reason: "no-mention" }, "skipping room message");
if (ctx.historyLimit > 0) {
const pendingText = (message.text ?? "").trim();
const fallbackFile = message.files?.[0]?.name
? `[Slack file: ${message.files[0].name}]`
: message.files?.length
? "[Slack file]"
: "";
const pendingBody = pendingText || fallbackFile;
if (pendingBody) {
recordPendingHistoryEntry({
historyMap: ctx.channelHistories,
historyKey,
limit: ctx.historyLimit,
entry: {
sender: senderName,
body: pendingBody,
timestamp: message.ts ? Math.round(Number(message.ts) * 1000) : undefined,
messageId: message.ts,
},
});
}
}
return null;
}
@@ -277,16 +315,6 @@ export async function prepareSlackMessage(params: {
: null;
const roomLabel = channelName ? `#${channelName}` : `#${message.channel}`;
const historyEntry =
isRoomish && ctx.historyLimit > 0
? {
sender: senderName,
body: rawBody,
timestamp: message.ts ? Math.round(Number(message.ts) * 1000) : undefined,
messageId: message.ts,
}
: undefined;
const preview = rawBody.replace(/\s+/g, " ").slice(0, 160);
const inboundLabel = isDirectMessage
? `Slack DM from ${senderName}`
@@ -297,18 +325,6 @@ export async function prepareSlackMessage(params: {
? `slack:channel:${message.channel}`
: `slack:group:${message.channel}`;
const baseSessionKey = route.sessionKey;
const threadTs = message.thread_ts;
const hasThreadTs = typeof threadTs === "string" && threadTs.length > 0;
const isThreadReply = hasThreadTs && (threadTs !== message.ts || Boolean(message.parent_user_id));
const threadKeys = resolveThreadSessionKeys({
baseSessionKey,
threadId: isThreadReply ? threadTs : undefined,
parentSessionKey: isThreadReply && ctx.threadInheritParent ? baseSessionKey : undefined,
});
const sessionKey = threadKeys.sessionKey;
const historyKey =
isThreadReply && ctx.threadHistoryScope === "thread" ? sessionKey : message.channel;
enqueueSystemEvent(`${inboundLabel}: ${preview}`, {
sessionKey,
contextKey: `slack:message:${message.channel}:${message.ts ?? "unknown"}`,
@@ -324,11 +340,10 @@ export async function prepareSlackMessage(params: {
let combinedBody = body;
if (isRoomish && ctx.historyLimit > 0) {
combinedBody = buildHistoryContextFromMap({
combinedBody = buildPendingHistoryContextFromMap({
historyMap: ctx.channelHistories,
historyKey,
limit: ctx.historyLimit,
entry: historyEntry,
currentMessage: combinedBody,
formatEntry: (entry) =>
formatAgentEnvelope({