From 5378583da1c1ad0f26b485663bdfa8e210e92111 Mon Sep 17 00:00:00 2001 From: Shadril Hassan Shifat <63901551+shadril238@users.noreply.github.com> Date: Sat, 14 Feb 2026 06:18:57 +0600 Subject: [PATCH] fix(discord): Apply historyLimit to channel/group sessions to prevent compaction bypass (openclaw#11356) thanks @shadril238 Verified: - pnpm build - pnpm check - pnpm test (ran; one unrelated existing failure in models forward-compat test) - pnpm vitest src/agents/pi-embedded-runner.history-limit-from-session-key.test.ts Co-authored-by: shadril238 <63901551+shadril238@users.noreply.github.com> Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com> --- CHANGELOG.md | 1 + ...efined-sessionkey-is-undefined.e2e.test.ts | 57 ++++++++++++++-- ...ner.history-limit-from-session-key.test.ts | 31 +++++++++ src/agents/pi-embedded-runner.ts | 1 + src/agents/pi-embedded-runner/history.ts | 66 ++++++++++++------- 5 files changed, 127 insertions(+), 29 deletions(-) create mode 100644 src/agents/pi-embedded-runner.history-limit-from-session-key.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 7264ce568..36164bb7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ Docs: https://docs.openclaw.ai - CLI: lazily load outbound provider dependencies and remove forced success-path exits so commands terminate naturally without killing intentional long-running foreground actions. (#12906) Thanks @DrCrinkle. - Clawdock: avoid Zsh readonly variable collisions in helper scripts. (#15501) Thanks @nkelner. - Discord: route autoThread replies to existing threads instead of the root channel. (#8302) Thanks @gavinbmoore, @thewilloftheshadow. +- Discord/Agents: apply channel/group `historyLimit` during embedded-runner history compaction to prevent long-running channel sessions from bypassing truncation and overflowing context windows. (#11224) Thanks @shadril238. - Agents/Image tool: cap image-analysis completion `maxTokens` by model capability (`min(4096, model.maxTokens)`) to avoid over-limit provider failures while still preventing truncation. (#11770) Thanks @detecti1. - TUI/Streaming: preserve richer streamed assistant text when final payload drops pre-tool-call text blocks, while keeping non-empty final payload authoritative for plain-text updates. (#15452) Thanks @TsekaLuk. - Inbound/Web UI: preserve literal `\n` sequences when normalizing inbound text so Windows paths like `C:\\Work\\nxxx\\README.md` are not corrupted. (#11547) Thanks @mcaxtr. diff --git a/src/agents/pi-embedded-runner.get-dm-history-limit-from-session-key.returns-undefined-sessionkey-is-undefined.e2e.test.ts b/src/agents/pi-embedded-runner.get-dm-history-limit-from-session-key.returns-undefined-sessionkey-is-undefined.e2e.test.ts index 15aece8c2..b685f88d4 100644 --- a/src/agents/pi-embedded-runner.get-dm-history-limit-from-session-key.returns-undefined-sessionkey-is-undefined.e2e.test.ts +++ b/src/agents/pi-embedded-runner.get-dm-history-limit-from-session-key.returns-undefined-sessionkey-is-undefined.e2e.test.ts @@ -143,14 +143,23 @@ describe("getDmHistoryLimitFromSessionKey", () => { 9, ); }); - it("returns undefined for non-dm session kinds", () => { + it("returns historyLimit for channel session kinds when configured", () => { const config = { channels: { - telegram: { dmHistoryLimit: 15 }, - slack: { dmHistoryLimit: 10 }, + slack: { historyLimit: 10, dmHistoryLimit: 15 }, + discord: { historyLimit: 8 }, }, } as OpenClawConfig; - expect(getDmHistoryLimitFromSessionKey("agent:beta:slack:channel:c1", config)).toBeUndefined(); + expect(getDmHistoryLimitFromSessionKey("agent:beta:slack:channel:c1", config)).toBe(10); + expect(getDmHistoryLimitFromSessionKey("discord:channel:123456", config)).toBe(8); + }); + it("returns undefined for non-dm/channel/group session kinds", () => { + const config = { + channels: { + telegram: { dmHistoryLimit: 15, historyLimit: 10 }, + }, + } as OpenClawConfig; + // "slash" is not dm, channel, or group expect(getDmHistoryLimitFromSessionKey("telegram:slash:123", config)).toBeUndefined(); }); it("returns undefined for unknown provider", () => { @@ -228,6 +237,46 @@ describe("getDmHistoryLimitFromSessionKey", () => { } as OpenClawConfig; expect(getDmHistoryLimitFromSessionKey("telegram:dm:123", config)).toBe(5); }); + it("returns historyLimit for channel sessions for all providers", () => { + const providers = [ + "telegram", + "whatsapp", + "discord", + "slack", + "signal", + "imessage", + "msteams", + "nextcloud-talk", + ] as const; + + for (const provider of providers) { + const config = { + channels: { [provider]: { historyLimit: 12 } }, + } as OpenClawConfig; + expect(getDmHistoryLimitFromSessionKey(`${provider}:channel:123`, config)).toBe(12); + expect(getDmHistoryLimitFromSessionKey(`agent:main:${provider}:channel:456`, config)).toBe( + 12, + ); + } + }); + it("returns historyLimit for group sessions", () => { + const config = { + channels: { + discord: { historyLimit: 15 }, + slack: { historyLimit: 10 }, + }, + } as OpenClawConfig; + expect(getDmHistoryLimitFromSessionKey("discord:group:123", config)).toBe(15); + expect(getDmHistoryLimitFromSessionKey("agent:main:slack:group:abc", config)).toBe(10); + }); + it("returns undefined for channel sessions when historyLimit is not configured", () => { + const config = { + channels: { + discord: { dmHistoryLimit: 10 }, // only dmHistoryLimit, no historyLimit + }, + } as OpenClawConfig; + expect(getDmHistoryLimitFromSessionKey("discord:channel:123", config)).toBeUndefined(); + }); describe("backward compatibility", () => { it("accepts both legacy :dm: and new :direct: session keys", () => { diff --git a/src/agents/pi-embedded-runner.history-limit-from-session-key.test.ts b/src/agents/pi-embedded-runner.history-limit-from-session-key.test.ts new file mode 100644 index 000000000..776c54f1c --- /dev/null +++ b/src/agents/pi-embedded-runner.history-limit-from-session-key.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from "vitest"; +import type { OpenClawConfig } from "../config/config.js"; +import { getDmHistoryLimitFromSessionKey } from "./pi-embedded-runner.js"; + +describe("getDmHistoryLimitFromSessionKey", () => { + it("keeps backward compatibility for dm/direct session kinds", () => { + const config = { + channels: { telegram: { dmHistoryLimit: 10 } }, + } as OpenClawConfig; + + expect(getDmHistoryLimitFromSessionKey("telegram:dm:123", config)).toBe(10); + expect(getDmHistoryLimitFromSessionKey("telegram:direct:123", config)).toBe(10); + }); + + it("returns historyLimit for channel and group session kinds", () => { + const config = { + channels: { discord: { historyLimit: 12, dmHistoryLimit: 5 } }, + } as OpenClawConfig; + + expect(getDmHistoryLimitFromSessionKey("discord:channel:123", config)).toBe(12); + expect(getDmHistoryLimitFromSessionKey("discord:group:456", config)).toBe(12); + }); + + it("returns undefined for unsupported session kinds", () => { + const config = { + channels: { discord: { historyLimit: 12, dmHistoryLimit: 5 } }, + } as OpenClawConfig; + + expect(getDmHistoryLimitFromSessionKey("discord:slash:123", config)).toBeUndefined(); + }); +}); diff --git a/src/agents/pi-embedded-runner.ts b/src/agents/pi-embedded-runner.ts index bdebd0005..4d968a9c2 100644 --- a/src/agents/pi-embedded-runner.ts +++ b/src/agents/pi-embedded-runner.ts @@ -5,6 +5,7 @@ export { applyExtraParamsToAgent, resolveExtraParams } from "./pi-embedded-runne export { applyGoogleTurnOrderingFix } from "./pi-embedded-runner/google.js"; export { getDmHistoryLimitFromSessionKey, + getHistoryLimitFromSessionKey, limitHistoryTurns, } from "./pi-embedded-runner/history.js"; export { resolveEmbeddedSessionLane } from "./pi-embedded-runner/lanes.js"; diff --git a/src/agents/pi-embedded-runner/history.ts b/src/agents/pi-embedded-runner/history.ts index 0340c315c..6515c0c13 100644 --- a/src/agents/pi-embedded-runner/history.ts +++ b/src/agents/pi-embedded-runner/history.ts @@ -38,8 +38,9 @@ export function limitHistoryTurns( /** * Extract provider + user ID from a session key and look up dmHistoryLimit. * Supports per-DM overrides and provider defaults. + * For channel/group sessions, uses historyLimit from provider config. */ -export function getDmHistoryLimitFromSessionKey( +export function getHistoryLimitFromSessionKey( sessionKey: string | undefined, config: OpenClawConfig | undefined, ): number | undefined { @@ -58,32 +59,17 @@ export function getDmHistoryLimitFromSessionKey( const kind = providerParts[1]?.toLowerCase(); const userIdRaw = providerParts.slice(2).join(":"); const userId = stripThreadSuffix(userIdRaw); - // Accept both "direct" (new) and "dm" (legacy) for backward compat - if (kind !== "direct" && kind !== "dm") { - return undefined; - } - - const getLimit = ( - providerConfig: - | { - dmHistoryLimit?: number; - dms?: Record; - } - | undefined, - ): number | undefined => { - if (!providerConfig) { - return undefined; - } - if (userId && providerConfig.dms?.[userId]?.historyLimit !== undefined) { - return providerConfig.dms[userId].historyLimit; - } - return providerConfig.dmHistoryLimit; - }; const resolveProviderConfig = ( cfg: OpenClawConfig | undefined, providerId: string, - ): { dmHistoryLimit?: number; dms?: Record } | undefined => { + ): + | { + historyLimit?: number; + dmHistoryLimit?: number; + dms?: Record; + } + | undefined => { const channels = cfg?.channels; if (!channels || typeof channels !== "object") { return undefined; @@ -92,8 +78,38 @@ export function getDmHistoryLimitFromSessionKey( if (!entry || typeof entry !== "object" || Array.isArray(entry)) { return undefined; } - return entry as { dmHistoryLimit?: number; dms?: Record }; + return entry as { + historyLimit?: number; + dmHistoryLimit?: number; + dms?: Record; + }; }; - return getLimit(resolveProviderConfig(config, provider)); + const providerConfig = resolveProviderConfig(config, provider); + if (!providerConfig) { + return undefined; + } + + // For DM sessions: per-DM override -> dmHistoryLimit. + // Accept both "direct" (new) and "dm" (legacy) for backward compat. + if (kind === "dm" || kind === "direct") { + if (userId && providerConfig.dms?.[userId]?.historyLimit !== undefined) { + return providerConfig.dms[userId].historyLimit; + } + return providerConfig.dmHistoryLimit; + } + + // For channel/group sessions: use historyLimit from provider config + // This prevents context overflow in long-running channel sessions + if (kind === "channel" || kind === "group") { + return providerConfig.historyLimit; + } + + return undefined; } + +/** + * @deprecated Use getHistoryLimitFromSessionKey instead. + * Alias for backward compatibility. + */ +export const getDmHistoryLimitFromSessionKey = getHistoryLimitFromSessionKey;