perf(slack): bound thread history pagination

This commit is contained in:
Ayaan Zaidi
2026-07-08 03:40:37 +05:30
parent 1993ead6f5
commit e3b43a0001
2 changed files with 65 additions and 3 deletions
@@ -997,6 +997,55 @@ describe("resolveSlackThreadHistory", () => {
]);
});
it("returns no thread history when pagination exceeds the bounded fetched window", async () => {
vi.mocked(logVerbose).mockClear();
const replies = vi
.fn()
.mockResolvedValueOnce({
messages: Array.from({ length: 200 }, (_, i) => ({
text: `msg-${i + 1}`,
user: "U1",
ts: `${i + 1}.000`,
})),
response_metadata: { next_cursor: "cursor-2" },
})
.mockResolvedValueOnce({
messages: Array.from({ length: 200 }, (_, i) => ({
text: `msg-${i + 201}`,
user: "U1",
ts: `${i + 201}.000`,
})),
response_metadata: { next_cursor: "cursor-3" },
})
.mockResolvedValueOnce({
messages: Array.from({ length: 200 }, (_, i) => ({
text: `msg-${i + 401}`,
user: "U1",
ts: `${i + 401}.000`,
})),
response_metadata: { next_cursor: "cursor-4" },
});
const client = {
conversations: { replies },
} as unknown as Parameters<typeof resolveSlackThreadHistory>[0]["client"];
const result = await resolveSlackThreadHistory({
channelId: "C1",
threadTs: "1.000",
client,
limit: 3,
});
expect(replies).toHaveBeenCalledTimes(3);
expect(requireMockCall(replies, 2, "conversations.replies")[0]).toMatchObject({
cursor: "cursor-3",
limit: 200,
});
expect(result).toEqual([]);
expectVerboseLogContains("slack thread history capped");
expectVerboseLogContains("channel=C1");
});
it("includes file-only messages and drops empty-only entries", async () => {
const replies = vi.fn().mockResolvedValueOnce({
messages: [
+16 -3
View File
@@ -185,12 +185,14 @@ type SlackRepliesPage = {
response_metadata?: { next_cursor?: string };
};
const SLACK_THREAD_HISTORY_MAX_PAGES = 3;
/**
* Fetches the most recent messages in a Slack thread (excluding the current message).
* Used to populate thread context when a new thread session starts.
*
* Uses cursor pagination and keeps only the latest N retained messages so long threads
* still produce up-to-date context without unbounded memory growth.
* Uses cursor pagination and keeps only the latest N retained messages when the full
* thread fits in the bounded fetch window.
*/
export async function resolveSlackThreadHistory(params: {
channelId: string;
@@ -208,9 +210,11 @@ export async function resolveSlackThreadHistory(params: {
const fetchLimit = 200;
const retained: SlackRepliesPageMessage[] = [];
let cursor: string | undefined;
let pagesFetched = 0;
try {
do {
pagesFetched += 1;
const response = (await params.client.conversations.replies({
channel: params.channelId,
ts: params.threadTs,
@@ -236,7 +240,16 @@ export async function resolveSlackThreadHistory(params: {
const next = response.response_metadata?.next_cursor;
cursor = typeof next === "string" && next.trim().length > 0 ? next.trim() : undefined;
} while (cursor);
// Slack replies paginate oldest to newest with no reverse cursor; cap cold
// thread seeding so pathological long threads cannot block dispatch.
} while (cursor && pagesFetched < SLACK_THREAD_HISTORY_MAX_PAGES);
if (cursor) {
logVerbose(
`slack thread history capped channel=${params.channelId} ts=${params.threadTs} pages=${SLACK_THREAD_HISTORY_MAX_PAGES}`,
);
return [];
}
return retained.map((msg) => ({
// For file-only messages, create a placeholder showing attached filenames.