From ab9ea827a4be34bc8036a5163132ce977e241513 Mon Sep 17 00:00:00 2001 From: Marc Terns Date: Sun, 11 Jan 2026 08:38:19 -0600 Subject: [PATCH] refactor: move dmHistoryLimit to provider-level config --- src/agents/pi-embedded-runner.test.ts | 63 +++++++++++++++++++++++++++ src/agents/pi-embedded-runner.ts | 47 +++++++++++++++++++- src/config/types.ts | 17 +++++++- src/config/zod-schema.ts | 9 +++- 4 files changed, 132 insertions(+), 4 deletions(-) diff --git a/src/agents/pi-embedded-runner.test.ts b/src/agents/pi-embedded-runner.test.ts index c558b80e1..c87fc47d0 100644 --- a/src/agents/pi-embedded-runner.test.ts +++ b/src/agents/pi-embedded-runner.test.ts @@ -11,6 +11,7 @@ import { applyGoogleTurnOrderingFix, buildEmbeddedSandboxInfo, createSystemPromptOverride, + getDmHistoryLimitFromSessionKey, limitHistoryTurns, runEmbeddedPiAgent, splitSdkTools, @@ -370,6 +371,68 @@ describe("limitHistoryTurns", () => { }); }); +describe("getDmHistoryLimitFromSessionKey", () => { + it("returns undefined when sessionKey is undefined", () => { + expect(getDmHistoryLimitFromSessionKey(undefined, {})).toBeUndefined(); + }); + + it("returns undefined when config is undefined", () => { + expect( + getDmHistoryLimitFromSessionKey("telegram:dm:123", undefined), + ).toBeUndefined(); + }); + + it("returns dmHistoryLimit for telegram provider", () => { + const config = { telegram: { dmHistoryLimit: 15 } } as ClawdbotConfig; + expect(getDmHistoryLimitFromSessionKey("telegram:dm:123", config)).toBe(15); + }); + + it("returns dmHistoryLimit for whatsapp provider", () => { + const config = { whatsapp: { dmHistoryLimit: 20 } } as ClawdbotConfig; + expect(getDmHistoryLimitFromSessionKey("whatsapp:dm:123", config)).toBe(20); + }); + + it("returns dmHistoryLimit for agent-prefixed session keys", () => { + const config = { telegram: { dmHistoryLimit: 10 } } as ClawdbotConfig; + expect( + getDmHistoryLimitFromSessionKey("agent:main:telegram:dm:123", config), + ).toBe(10); + }); + + it("returns undefined for unknown provider", () => { + const config = { telegram: { dmHistoryLimit: 15 } } as ClawdbotConfig; + expect( + getDmHistoryLimitFromSessionKey("unknown:dm:123", config), + ).toBeUndefined(); + }); + + it("returns undefined when provider config has no dmHistoryLimit", () => { + const config = { telegram: {} } as ClawdbotConfig; + expect( + getDmHistoryLimitFromSessionKey("telegram:dm:123", config), + ).toBeUndefined(); + }); + + it("handles all supported providers", () => { + const providers = [ + "telegram", + "whatsapp", + "discord", + "slack", + "signal", + "imessage", + "msteams", + ] as const; + + for (const provider of providers) { + const config = { [provider]: { dmHistoryLimit: 5 } } as ClawdbotConfig; + expect( + getDmHistoryLimitFromSessionKey(`${provider}:dm:123`, config), + ).toBe(5); + } + }); +}); + describe("runEmbeddedPiAgent", () => { it("writes models.json into the provided agentDir", async () => { const agentDir = await fs.mkdtemp( diff --git a/src/agents/pi-embedded-runner.ts b/src/agents/pi-embedded-runner.ts index 98fb598a4..862a6364f 100644 --- a/src/agents/pi-embedded-runner.ts +++ b/src/agents/pi-embedded-runner.ts @@ -445,6 +445,49 @@ export function limitHistoryTurns( return messages; } +/** + * Extracts the provider name from a session key and looks up dmHistoryLimit + * from the provider config. + * + * Session key formats: + * - `telegram:dm:123` → provider = telegram + * - `agent:main:telegram:dm:123` → provider = telegram (skip "agent::") + */ +export function getDmHistoryLimitFromSessionKey( + sessionKey: string | undefined, + config: ClawdbotConfig | undefined, +): number | undefined { + if (!sessionKey || !config) return undefined; + + const parts = sessionKey.split(":").filter(Boolean); + // Handle agent-prefixed keys: agent:::... + const providerParts = + parts.length >= 3 && parts[0] === "agent" ? parts.slice(2) : parts; + + const provider = providerParts[0]?.toLowerCase(); + if (!provider) return undefined; + + // Map provider to config key + switch (provider) { + case "telegram": + return config.telegram?.dmHistoryLimit; + case "whatsapp": + return config.whatsapp?.dmHistoryLimit; + case "discord": + return config.discord?.dmHistoryLimit; + case "slack": + return config.slack?.dmHistoryLimit; + case "signal": + return config.signal?.dmHistoryLimit; + case "imessage": + return config.imessage?.dmHistoryLimit; + case "msteams": + return config.msteams?.dmHistoryLimit; + default: + return undefined; + } +} + const ACTIVE_EMBEDDED_RUNS = new Map(); type EmbeddedRunWaiter = { resolve: (ended: boolean) => void; @@ -1060,7 +1103,7 @@ export async function compactEmbeddedPiSession(params: { const validated = validateGeminiTurns(prior); const limited = limitHistoryTurns( validated, - params.config?.session?.dmHistoryLimit, + getDmHistoryLimitFromSessionKey(params.sessionKey, params.config), ); if (limited.length > 0) { session.agent.replaceMessages(limited); @@ -1455,7 +1498,7 @@ export async function runEmbeddedPiAgent(params: { const validated = validateGeminiTurns(prior); const limited = limitHistoryTurns( validated, - params.config?.session?.dmHistoryLimit, + getDmHistoryLimitFromSessionKey(params.sessionKey, params.config), ); if (limited.length > 0) { session.agent.replaceMessages(limited); diff --git a/src/config/types.ts b/src/config/types.ts index 2419b9499..e748ec119 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -57,7 +57,6 @@ export type SessionConfig = { resetTriggers?: string[]; idleMinutes?: number; heartbeatIdleMinutes?: number; - dmHistoryLimit?: number; store?: string; typingIntervalSeconds?: number; typingMode?: TypingMode; @@ -148,6 +147,8 @@ export type WhatsAppConfig = { groupPolicy?: GroupPolicy; /** Max group messages to keep as history context (0 disables). */ historyLimit?: number; + /** Max DM turns to keep as history context. */ + dmHistoryLimit?: number; /** Outbound text chunk size (chars). Default: 4000. */ textChunkLimit?: number; /** Maximum media file size in MB. Default: 50. */ @@ -201,6 +202,8 @@ export type WhatsAppAccountConfig = { groupPolicy?: GroupPolicy; /** Max group messages to keep as history context (0 disables). */ historyLimit?: number; + /** Max DM turns to keep as history context. */ + dmHistoryLimit?: number; textChunkLimit?: number; mediaMaxMb?: number; blockStreaming?: boolean; @@ -380,6 +383,8 @@ export type TelegramAccountConfig = { groupPolicy?: GroupPolicy; /** Max group messages to keep as history context (0 disables). */ historyLimit?: number; + /** Max DM turns to keep as history context. */ + dmHistoryLimit?: number; /** Outbound text chunk size (chars). Default: 4000. */ textChunkLimit?: number; /** Disable block streaming for this account. */ @@ -523,6 +528,8 @@ export type DiscordAccountConfig = { maxLinesPerMessage?: number; mediaMaxMb?: number; historyLimit?: number; + /** Max DM turns to keep as history context. */ + dmHistoryLimit?: number; /** Retry policy for outbound Discord API calls. */ retry?: OutboundRetryConfig; /** Per-action tool gating (default: true for all). */ @@ -619,6 +626,8 @@ export type SlackAccountConfig = { groupPolicy?: GroupPolicy; /** Max channel messages to keep as history context (0 disables). */ historyLimit?: number; + /** Max DM turns to keep as history context. */ + dmHistoryLimit?: number; textChunkLimit?: number; blockStreaming?: boolean; /** Merge streamed block replies before sending. */ @@ -678,6 +687,8 @@ export type SignalAccountConfig = { groupPolicy?: GroupPolicy; /** Max group messages to keep as history context (0 disables). */ historyLimit?: number; + /** Max DM turns to keep as history context. */ + dmHistoryLimit?: number; /** Outbound text chunk size (chars). Default: 4000. */ textChunkLimit?: number; blockStreaming?: boolean; @@ -753,6 +764,8 @@ export type MSTeamsConfig = { requireMention?: boolean; /** Max group/channel messages to keep as history context (0 disables). */ historyLimit?: number; + /** Max DM turns to keep as history context. */ + dmHistoryLimit?: number; /** Default reply style: "thread" replies to the message, "top-level" posts a new message. */ replyStyle?: MSTeamsReplyStyle; /** Per-team config. Key is team ID (from the /team/ URL path segment). */ @@ -789,6 +802,8 @@ export type IMessageAccountConfig = { groupPolicy?: GroupPolicy; /** Max group messages to keep as history context (0 disables). */ historyLimit?: number; + /** Max DM turns to keep as history context. */ + dmHistoryLimit?: number; /** Include attachments + reactions in watch payloads. */ includeAttachments?: boolean; /** Max outbound media size in MB. */ diff --git a/src/config/zod-schema.ts b/src/config/zod-schema.ts index 5e081336d..3d7e99da2 100644 --- a/src/config/zod-schema.ts +++ b/src/config/zod-schema.ts @@ -273,6 +273,7 @@ const TelegramAccountSchemaBase = z.object({ groupAllowFrom: z.array(z.union([z.string(), z.number()])).optional(), groupPolicy: GroupPolicySchema.optional().default("open"), historyLimit: z.number().int().min(0).optional(), + dmHistoryLimit: z.number().int().min(0).optional(), textChunkLimit: z.number().int().positive().optional(), blockStreaming: z.boolean().optional(), draftChunk: BlockStreamingChunkSchema.optional(), @@ -362,6 +363,7 @@ const DiscordAccountSchema = z.object({ token: z.string().optional(), groupPolicy: GroupPolicySchema.optional().default("open"), historyLimit: z.number().int().min(0).optional(), + dmHistoryLimit: z.number().int().min(0).optional(), textChunkLimit: z.number().int().positive().optional(), blockStreaming: z.boolean().optional(), blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(), @@ -434,6 +436,7 @@ const SlackAccountSchema = z.object({ allowBots: z.boolean().optional(), groupPolicy: GroupPolicySchema.optional().default("open"), historyLimit: z.number().int().min(0).optional(), + dmHistoryLimit: z.number().int().min(0).optional(), textChunkLimit: z.number().int().positive().optional(), blockStreaming: z.boolean().optional(), blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(), @@ -488,6 +491,7 @@ const SignalAccountSchemaBase = z.object({ groupAllowFrom: z.array(z.union([z.string(), z.number()])).optional(), groupPolicy: GroupPolicySchema.optional().default("open"), historyLimit: z.number().int().min(0).optional(), + dmHistoryLimit: z.number().int().min(0).optional(), textChunkLimit: z.number().int().positive().optional(), blockStreaming: z.boolean().optional(), blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(), @@ -536,6 +540,7 @@ const IMessageAccountSchemaBase = z.object({ groupAllowFrom: z.array(z.union([z.string(), z.number()])).optional(), groupPolicy: GroupPolicySchema.optional().default("open"), historyLimit: z.number().int().min(0).optional(), + dmHistoryLimit: z.number().int().min(0).optional(), includeAttachments: z.boolean().optional(), mediaMaxMb: z.number().int().positive().optional(), textChunkLimit: z.number().int().positive().optional(), @@ -610,6 +615,7 @@ const MSTeamsConfigSchema = z mediaAllowHosts: z.array(z.string()).optional(), requireMention: z.boolean().optional(), historyLimit: z.number().int().min(0).optional(), + dmHistoryLimit: z.number().int().min(0).optional(), replyStyle: MSTeamsReplyStyleSchema.optional(), teams: z.record(z.string(), MSTeamsTeamSchema.optional()).optional(), }) @@ -630,7 +636,6 @@ const SessionSchema = z resetTriggers: z.array(z.string()).optional(), idleMinutes: z.number().int().positive().optional(), heartbeatIdleMinutes: z.number().int().positive().optional(), - dmHistoryLimit: z.number().int().positive().optional(), store: z.string().optional(), typingIntervalSeconds: z.number().int().positive().optional(), typingMode: z @@ -1355,6 +1360,7 @@ export const ClawdbotSchema = z groupAllowFrom: z.array(z.string()).optional(), groupPolicy: GroupPolicySchema.optional().default("open"), historyLimit: z.number().int().min(0).optional(), + dmHistoryLimit: z.number().int().min(0).optional(), textChunkLimit: z.number().int().positive().optional(), mediaMaxMb: z.number().int().positive().optional(), blockStreaming: z.boolean().optional(), @@ -1404,6 +1410,7 @@ export const ClawdbotSchema = z groupAllowFrom: z.array(z.string()).optional(), groupPolicy: GroupPolicySchema.optional().default("open"), historyLimit: z.number().int().min(0).optional(), + dmHistoryLimit: z.number().int().min(0).optional(), textChunkLimit: z.number().int().positive().optional(), mediaMaxMb: z.number().int().positive().optional().default(50), blockStreaming: z.boolean().optional(),