fix: group chats stall when historyLimit is the JSON integer maximum (#154305)

* fix: group chats stall when historyLimit is the JSON integer maximum

JSON schema allows 9007199254740991, which channels treated as an unbounded
chat window and stuffed into the model prompt. Treat that sentinel as unset,
cap prompt windows, and have doctor strip it.

* fix: preserve bounded history settings through Doctor

Share one private numeric history policy without expanding the public SDK. Preserve inherited and explicit-zero settings during Telegram repair, document the ceiling, and retain shipped compatibility exports.

Co-authored-by: VACInc <3279061+VACInc@users.noreply.github.com>

* fix: keep history proof behind the Doctor boundary

Exercise Telegram repair through the existing scoped production dispatcher rather than a forbidden extension import. Regenerate bundled channel metadata so the verified observed-message and transcript guidance reaches consumers. Preserve all runtime behavior and boundary guards.

Co-authored-by: VACInc <3279061+VACInc@users.noreply.github.com>

* test: observe durable approval and recovery completion

Reuse the scoped fixture-owner repair published in PR #153829 at d8d7c540cdd62f0fb15017a31e703fe82f146a51. Observe real RPC acceptance, approval publication, committed session state, and queued admission; join operations before cleanup. Preserve all authority, cancellation, durable-state, and call-count assertions without changing production behavior or timeouts.

Co-authored-by: VACInc <3279061+VACInc@users.noreply.github.com>

* test: await exec approval acceptance before assertions

Reuse the existing fixture owner published in PR #153829 at d8d7c540cdd62f0fb15017a31e703fe82f146a51. Wait for real acceptance instead of short pending-record and broadcast polling, preserve all sanitization, provenance, and cancellation assertions, and join pending requests before database cleanup. No production or timeout change.

Co-authored-by: VACInc <3279061+VACInc@users.noreply.github.com>

* test: prepare approval worker before identity deadlines

Use the existing prepared approval fixture before the unchanged two-second registration deadline starts. Preserve revoked-generation rejection and replacement-generation allow-once assertions. Controlled first-owner latency reproduces the old expired-but-authorized pending-card failure; identical latency before record creation passes. Diagnostic instrumentation is not retained.

Co-authored-by: VACInc <3279061+VACInc@users.noreply.github.com>

---------

Co-authored-by: VACInc <3279061+VACInc@users.noreply.github.com>
Co-authored-by: roboclaw-bot <309084314+roboclaw-bot@users.noreply.github.com>
This commit is contained in:
Vito Cappello 2026-09-21 19:41:35 -04:00 • committed by GitHub
parent 6ee9578ab7
commit 208132c299
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
37 changed files with 424 additions and 121 deletions

View file

@ -1,4 +1,4 @@
5b0264d70045ae2c67389676ebbb655abcdd20d7424081b2f69fe316ce91aea5 config-baseline.json
248b8107e0962e67dccf7ef5dd7381efee9c6431a6eddcce5f19059b791f769f config-baseline.core.json
51c84b118b136dfff84993e3cee0ff76ee06b614842b125d0fde110635fc10b8 config-baseline.channel.json
a3367695b30a1ac4c2088e92a043ed2f472472ea11bcf98e7b8fc15f3ff18bd5 config-baseline.json
ef39b909d71c578cc93922509ca4a0819eda4c8024784bfebcf91d2fd44997c5 config-baseline.core.json
93b1f2e7e7121bf82d2d571bfa0c01ce710ae96185f363da1ffbc241afdf4549 config-baseline.channel.json
b763248bcc344ef5dcd5706ff7b6814f40b81d81e8f6ae3259ccaf818c53e102 config-baseline.plugin.json

View file

@ -63,6 +63,17 @@ The marker is durable and keyed by account, workspace, and Slack app ID, so Agen
Admitted channel and group turns fetch a recent window from Slack, including after
a Gateway restart. `channels.slack.historyLimit` bounds the window (default `50`,
with `messages.groupChat.historyLimit` as a fallback). Account overrides apply.
Automatic observed-message windows are capped at 200 messages; the JSON integer
maximum (`9007199254740991`) selects the 50-message default.
For observed DM context, `dmHistoryLimit` and `dms.<userId>.historyLimit` use a
default of `0` (disabled) and a maximum of 200 messages. The JSON integer maximum
selects that disabled default, including for a per-DM override.
These saved fields also control embedded session transcript trimming in user
turns, where `0` means no trimming and the existing eviction cushion still
applies. Doctor preserves the configured values; the observed-message ceiling
does not impose a new transcript-turn ceiling.
With `requireMention: true`, messages that do not satisfy the configured mention
or implicit-mention gates do not start agent turns or automatic history reads.

View file

@ -208,7 +208,7 @@ Active turns keep their captured delivery settings.
<Accordion title="Retained group history">
Telegram groups and forum topics use a recent automatic context window plus explicit history reads. With `requireMention: true`, permitted unmentioned messages are recorded without starting agent turns. A later addressed turn receives recent context, and the agent can use `message(action="read")` when it needs earlier discussion.
- `channels.telegram.historyLimit` or `messages.groupChat.historyLimit` caps the automatic window (default 50). `0` disables automatic history injection, not recording or explicit reads.
- `channels.telegram.historyLimit` or `messages.groupChat.historyLimit` caps the automatic observed-message window (default 50, hard ceiling 200). `0` disables automatic history injection, not recording or explicit reads. The JSON integer maximum (`9007199254740991`) selects the 50-message default.
- Automatic context examines a bounded recent slice before applying topic and sender permissions. A busy group can supply fewer than the configured number of messages when other topics or excluded senders dominate that slice. The agent can page farther back with explicit history reads.
- History reads stay within the authorized account, chat, and topic. They use Telegram message IDs for references and paging; omitting a topic must not expand a topic-scoped read to the whole group.
- Agent reads default to 50 messages per page, up to 100. Use `before` with the returned `oldestMessageId` to read older messages, `after` to read newer messages, or `messageId` for an exact reference. These reads require the agent's authenticated current group/topic; they do not fetch Telegram server history.
@ -232,7 +232,8 @@ Active turns keep their captured delivery settings.
- automatic group context uses `channels.telegram.historyLimit` or `messages.groupChat.historyLimit` (default 50); `0` disables the automatic window, not retained history.
- reply/quote/forward supplemental context normalizes into one selected conversation context window when the gateway has observed the parent messages; the observed-message cache lives in OpenClaw SQLite plugin state, and `openclaw doctor --fix` imports legacy sidecars. Telegram only includes one shallow `reply_to_message` per update, so chains older than the cache are limited to that payload.
- Telegram allowlists primarily gate who can trigger the agent, not a full supplemental-context redaction boundary.
- DM history: `channels.telegram.dmHistoryLimit`, `channels.telegram.dms["<user_id>"].historyLimit`.
- DM history: `channels.telegram.dmHistoryLimit`, `channels.telegram.dms["<user_id>"].historyLimit`. Automatic observed-DM context defaults to 10 messages and is capped at 200; `0` disables that extra context, and the JSON integer maximum selects the default.
- These channel/account/DM fields also control embedded session transcript trimming in **user turns**, not observed messages. That separate limiter treats `0` as no trimming and keeps its existing eviction cushion. Doctor preserves valid saved limits, including the JSON integer maximum, so it does not silently change transcript context.
CLI and message-tool send targets accept a numeric chat ID, username, or forum topic target:

View file

@ -1,7 +1,8 @@
// Discord provider module implements model/runtime integration.
import type { ChannelRuntimeSurface } from "openclaw/plugin-sdk/channel-contract";
import type { PluginRuntime } from "openclaw/plugin-sdk/channel-core";
import type { OpenClawConfig, ReplyToMode } from "openclaw/plugin-sdk/config-contracts";
// Discord provider module implements model/runtime integration.
import { resolvePromptHistoryLimit } from "openclaw/plugin-sdk/number-runtime";
import { resolveTextChunkLimit } from "openclaw/plugin-sdk/reply-chunking";
import {
createRuntimeConfigReader,
@ -140,9 +141,9 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
const textLimit = resolveTextChunkLimit(cfg, "discord", account.accountId, {
fallbackLimit: 2000,
});
const historyLimit = Math.max(
0,
opts.historyLimit ?? discordCfg.historyLimit ?? cfg.messages?.groupChat?.historyLimit ?? 20,
const historyLimit = resolvePromptHistoryLimit(
opts.historyLimit ?? discordCfg.historyLimit ?? cfg.messages?.groupChat?.historyLimit,
20,
);
const replyToMode = opts.replyToMode ?? discordCfg.replyToMode ?? "off";
const dmEnabled = dmConfig?.enabled ?? true;

View file

@ -15,12 +15,11 @@ import {
resolveConfiguredBindingRoute,
resolveRuntimeConversationBindingRoute,
} from "openclaw/plugin-sdk/conversation-runtime";
import { parseStrictNonNegativeInteger } from "openclaw/plugin-sdk/number-runtime";
import {
DEFAULT_GROUP_HISTORY_LIMIT,
createChannelHistoryWindow,
type HistoryEntry,
} from "openclaw/plugin-sdk/reply-history";
resolvePromptHistoryLimit,
parseStrictNonNegativeInteger,
} from "openclaw/plugin-sdk/number-runtime";
import { createChannelHistoryWindow, type HistoryEntry } from "openclaw/plugin-sdk/reply-history";
import { resolveInboundLastRouteSessionKey } from "openclaw/plugin-sdk/routing";
import {
resolveDefaultGroupPolicy,
@ -486,9 +485,8 @@ export async function handleFeishuMessage(params: {
log(`feishu[${account.accountId}]: detected @ forward request, targets: [${names}]`);
}
const historyLimit = Math.max(
0,
feishuCfg?.historyLimit ?? cfg.messages?.groupChat?.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT,
const historyLimit = resolvePromptHistoryLimit(
feishuCfg?.historyLimit ?? cfg.messages?.groupChat?.historyLimit,
);
const groupConfig = isGroup
? resolveFeishuGroupConfig({ cfg: feishuCfg, groupId: ctx.chatId })

View file

@ -1,4 +1,3 @@
// Imessage provider module implements model/runtime integration.
import { resolveAgentConfig, resolveHumanDelayConfig } from "openclaw/plugin-sdk/agent-runtime";
import { CHANNEL_APPROVAL_NATIVE_RUNTIME_CONTEXT_CAPABILITY } from "openclaw/plugin-sdk/approval-handler-runtime";
import type { PluginRuntime } from "openclaw/plugin-sdk/channel-core";
@ -34,7 +33,9 @@ import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
import { channelReadyPatch } from "openclaw/plugin-sdk/gateway-runtime";
import { redactIdentifier } from "openclaw/plugin-sdk/logging-core";
import { isInboundPathAllowed, kindFromMime } from "openclaw/plugin-sdk/media-runtime";
import { DEFAULT_GROUP_HISTORY_LIMIT, type HistoryEntry } from "openclaw/plugin-sdk/reply-history";
// Imessage provider module implements model/runtime integration.
import { resolvePromptHistoryLimit } from "openclaw/plugin-sdk/number-runtime";
import type { HistoryEntry } from "openclaw/plugin-sdk/reply-history";
import { resolveTextChunkLimit, type GetReplyOptions } from "openclaw/plugin-sdk/reply-runtime";
import { resolveInboundLastRouteSessionKey } from "openclaw/plugin-sdk/routing";
import {
@ -353,11 +354,8 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P
capability: CHANNEL_APPROVAL_GATEWAY_RUNTIME_CONTEXT_CAPABILITY,
});
const imessageCfg = accountInfo.config;
const historyLimit = Math.max(
0,
imessageCfg.historyLimit ??
cfg.messages?.groupChat?.historyLimit ??
DEFAULT_GROUP_HISTORY_LIMIT,
const historyLimit = resolvePromptHistoryLimit(
imessageCfg.historyLimit ?? cfg.messages?.groupChat?.historyLimit,
);
const groupHistories = new Map<string, HistoryEntry[]>();
const sentMessageCache = createSentMessageCache();

View file

@ -1,5 +1,6 @@
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { DEFAULT_GROUP_HISTORY_LIMIT, type HistoryEntry } from "openclaw/plugin-sdk/reply-history";
import { resolvePromptHistoryLimit } from "openclaw/plugin-sdk/number-runtime";
import type { HistoryEntry } from "openclaw/plugin-sdk/reply-history";
import {
getRuntimeConfig,
getRuntimeConfigSnapshot,
@ -106,10 +107,9 @@ export function createLineBot(opts: LineBotOptions): LineBot {
: {}),
...(control.missingParts === undefined ? {} : { missingParts: control.missingParts }),
groupHistories,
historyLimit:
account.config.historyLimit ??
cfg.messages?.groupChat?.historyLimit ??
DEFAULT_GROUP_HISTORY_LIMIT,
historyLimit: resolvePromptHistoryLimit(
account.config.historyLimit ?? cfg.messages?.groupChat?.historyLimit,
),
});
},
});

View file

@ -7,7 +7,10 @@ import {
waitUntilAbort,
} from "openclaw/plugin-sdk/channel-outbound";
import { registerChannelRuntimeContext } from "openclaw/plugin-sdk/channel-runtime-context";
import { resolveOptionalIntegerOption } from "openclaw/plugin-sdk/number-runtime";
import {
resolvePromptHistoryLimit,
resolveOptionalIntegerOption,
} from "openclaw/plugin-sdk/number-runtime";
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime";
import {
GROUP_POLICY_BLOCKED_LABEL,
@ -265,7 +268,10 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
const globalGroupChatHistoryLimit = (
cfg.messages as { groupChat?: { historyLimit?: number } } | undefined
)?.groupChat?.historyLimit;
const historyLimit = Math.max(0, accountConfig.historyLimit ?? globalGroupChatHistoryLimit ?? 0);
const historyLimit = resolvePromptHistoryLimit(
accountConfig.historyLimit ?? globalGroupChatHistoryLimit,
0,
);
const mediaMaxMb = opts.mediaMaxMb ?? accountConfig.mediaMaxMb ?? DEFAULT_MEDIA_MAX_MB;
const mediaMaxBytes = Math.max(1, mediaMaxMb) * 1024 * 1024;
const streaming = resolveMatrixStreamingMode(accountConfig.streaming);

View file

@ -1,4 +1,3 @@
// Mattermost plugin module normalizes accepted posts into inbound turns.
import {
formatInboundEnvelope,
implicitMentionKindWhen,
@ -8,6 +7,8 @@ import {
resolveChannelGroups,
resolveChannelGroupsConfigPath,
} from "openclaw/plugin-sdk/channel-policy";
// Mattermost plugin module normalizes accepted posts into inbound turns.
import { resolvePromptHistoryLimit } from "openclaw/plugin-sdk/number-runtime";
import { resolvePinnedMainDmOwnerFromAllowlist } from "openclaw/plugin-sdk/security-runtime";
import {
normalizeOptionalString,
@ -42,8 +43,8 @@ import { dispatchMattermostInboundTurn } from "./monitor-turn.js";
import type { MattermostMonitorContext } from "./monitor-types.js";
import type { MattermostEventPayload } from "./monitor-websocket.js";
import {
createChannelHistoryWindow,
DEFAULT_GROUP_HISTORY_LIMIT,
createChannelHistoryWindow,
logInboundDrop,
type HistoryEntry,
} from "./runtime-api.js";
@ -60,11 +61,9 @@ export function createMattermostPostHandler(monitor: MattermostMonitorContext) {
});
const { resolveMattermostMedia, resolveUserInfo } = resources;
const channelHistories = new Map<string, HistoryEntry[]>();
const historyLimit = Math.max(
0,
account.config.historyLimit ??
cfg.messages?.groupChat?.historyLimit ??
DEFAULT_GROUP_HISTORY_LIMIT,
const historyLimit = resolvePromptHistoryLimit(
account.config.historyLimit ?? cfg.messages?.groupChat?.historyLimit,
DEFAULT_GROUP_HISTORY_LIMIT,
);
const recoverThread = createMattermostThreadBackfill({ monitor, channelHistories, historyLimit });

View file

@ -4,11 +4,8 @@ import {
} from "openclaw/plugin-sdk/channel-inbound";
import { fanInChannelIngressLifecycles } from "openclaw/plugin-sdk/channel-ingress-runtime";
import { resolveChannelContextVisibilityMode } from "openclaw/plugin-sdk/context-visibility-runtime";
import {
DEFAULT_GROUP_HISTORY_LIMIT,
createChannelHistoryWindow,
type HistoryEntry,
} from "openclaw/plugin-sdk/reply-history";
import { resolvePromptHistoryLimit } from "openclaw/plugin-sdk/number-runtime";
import { createChannelHistoryWindow, type HistoryEntry } from "openclaw/plugin-sdk/reply-history";
import { createRuntimeConfigReader } from "openclaw/plugin-sdk/runtime-config-snapshot";
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
import { formatUnknownError } from "../errors.js";
@ -53,11 +50,8 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
cfg,
channel: "msteams",
});
const historyLimit = Math.max(
0,
msteamsCfg?.historyLimit ??
cfg.messages?.groupChat?.historyLimit ??
DEFAULT_GROUP_HISTORY_LIMIT,
const historyLimit = resolvePromptHistoryLimit(
msteamsCfg?.historyLimit ?? cfg.messages?.groupChat?.historyLimit,
);
const conversationHistories = new Map<string, HistoryEntry[]>();
const readConfig = createRuntimeConfigReader(cfg);

View file

@ -1,4 +1,3 @@
// Signal plugin module implements monitor behavior.
import { CHANNEL_APPROVAL_NATIVE_RUNTIME_CONTEXT_CAPABILITY } from "openclaw/plugin-sdk/approval-handler-adapter-runtime";
import type { PluginRuntime } from "openclaw/plugin-sdk/channel-core";
import { resolveChannelStreamingBlockEnabled } from "openclaw/plugin-sdk/channel-outbound";
@ -13,7 +12,9 @@ import {
estimateBase64DecodedBytes,
saveMediaBuffer,
} from "openclaw/plugin-sdk/media-runtime";
import { DEFAULT_GROUP_HISTORY_LIMIT, type HistoryEntry } from "openclaw/plugin-sdk/reply-history";
// Signal plugin module implements monitor behavior.
import { resolvePromptHistoryLimit } from "openclaw/plugin-sdk/number-runtime";
import type { HistoryEntry } from "openclaw/plugin-sdk/reply-history";
import {
deliverTextOrMediaReply,
resolveSendableOutboundReplyParts,
@ -396,11 +397,8 @@ export async function monitorSignalProvider(opts: MonitorSignalOpts = {}): Promi
cfg,
accountId: opts.accountId,
});
const historyLimit = Math.max(
0,
accountInfo.config.historyLimit ??
cfg.messages?.groupChat?.historyLimit ??
DEFAULT_GROUP_HISTORY_LIMIT,
const historyLimit = resolvePromptHistoryLimit(
accountInfo.config.historyLimit ?? cfg.messages?.groupChat?.historyLimit,
);
const groupHistories = new Map<string, HistoryEntry[]>();
const textLimit = resolveTextChunkLimit(cfg, "signal", accountInfo.accountId);

View file

@ -1,7 +1,20 @@
import { createChannelConfigUiHints } from "openclaw/plugin-sdk/channel-core";
import type { ChannelConfigUiHint } from "openclaw/plugin-sdk/channel-core";
const observedGroupHistoryHint = {
help: "Automatic observed-message context uses a default of 50 and a maximum of 200 messages; 0 disables automatic injection. The JSON integer maximum selects the 50-message default. Session transcript trimming separately counts user turns, where 0 means no trimming. The observed-message cap does not rewrite saved values.",
};
const observedDmHistoryHint = {
help: "Automatic observed-DM context uses a default of 0 and a maximum of 200 messages; 0 disables that extra context. The JSON integer maximum selects the 0-message default. Session transcript trimming separately counts user turns, where 0 means no trimming. The observed-message cap does not rewrite saved values.",
};
export const slackChannelConfigUiHints = {
historyLimit: observedGroupHistoryHint,
"accounts.*.historyLimit": observedGroupHistoryHint,
dmHistoryLimit: observedDmHistoryHint,
"accounts.*.dmHistoryLimit": observedDmHistoryHint,
"dms.*.historyLimit": observedDmHistoryHint,
"accounts.*.dms.*.historyLimit": observedDmHistoryHint,
"": {
label: "Slack",
help: "Slack channel provider configuration for bot/app tokens, streaming behavior, and DM policy controls. Keep token handling and thread behavior explicit to avoid noisy workspace interactions.",

View file

@ -0,0 +1,45 @@
import { WebClient } from "@slack/web-api";
import { describe, expect, it, vi } from "vitest";
import { resolveSlackDmHistoryContext, resolveSlackDmHistoryLimit } from "./prepare-dm-history.js";
import { createInboundSlackTestContext, createSlackTestAccount } from "./prepare.test-helpers.js";
describe("Slack observed DM history limits", () => {
it.each([
{ override: Number.MAX_SAFE_INTEGER, defaultLimit: 7, expected: 0 },
{ override: 5000, defaultLimit: 7, expected: 200 },
{ override: 0, defaultLimit: 7, expected: 0 },
{ override: 3, defaultLimit: 7, expected: 3 },
{ override: undefined, defaultLimit: 7, expected: 7 },
{ override: undefined, defaultLimit: 5000, expected: 200 },
])("bounds the selected window $override / $defaultLimit to $expected", async (testCase) => {
const account = createSlackTestAccount({
dms: { U1: { historyLimit: testCase.override } },
});
const limit = resolveSlackDmHistoryLimit({
account,
userId: "U1",
defaultLimit: testCase.defaultLimit,
});
expect(limit).toBe(testCase.expected);
const client = new WebClient();
const history = vi
.spyOn(client.conversations, "history")
.mockResolvedValue({ ok: true, messages: [] });
const ctx = createInboundSlackTestContext({ cfg: {}, appClient: client });
await resolveSlackDmHistoryContext({
ctx,
channelId: "D1",
currentMessageTs: "42.0",
limit,
envelopeOptions: {},
});
if (testCase.expected === 0) {
expect(history).not.toHaveBeenCalled();
} else {
expect(history).toHaveBeenCalledOnce();
expect(history).toHaveBeenCalledWith(
expect.objectContaining({ channel: "D1", limit: testCase.expected + 1 }),
);
}
});
});

View file

@ -1,5 +1,6 @@
import { formatInboundEnvelope } from "openclaw/plugin-sdk/channel-inbound";
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import { resolvePromptHistoryLimit } from "openclaw/plugin-sdk/number-runtime";
import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import type { ResolvedSlackAccount } from "../../accounts.js";
@ -30,7 +31,7 @@ export function resolveSlackDmHistoryLimit(params: {
params.userId && params.account.config.dms?.[params.userId]?.historyLimit !== undefined
? params.account.config.dms[params.userId]?.historyLimit
: undefined;
return Math.max(0, override ?? params.defaultLimit);
return resolvePromptHistoryLimit(override ?? params.defaultLimit, 0);
}
export async function resolveSlackDmHistoryContext(params: {

View file

@ -6,8 +6,8 @@ import {
summarizeMapping,
} from "openclaw/plugin-sdk/allow-from";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { resolvePromptHistoryLimit } from "openclaw/plugin-sdk/number-runtime";
import { resolveTextChunkLimit } from "openclaw/plugin-sdk/reply-chunking";
import { DEFAULT_GROUP_HISTORY_LIMIT } from "openclaw/plugin-sdk/reply-history";
import { normalizeMainKey } from "openclaw/plugin-sdk/routing";
import { createRuntimeConfigReader } from "openclaw/plugin-sdk/runtime-config-snapshot";
import { warn, type RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
@ -70,11 +70,10 @@ export function resolveSlackMonitorPolicy(
log: (message) => runtime?.log?.(warn(message)),
});
return {
historyLimit: Math.max(
0,
slack.historyLimit ?? cfg.messages?.groupChat?.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT,
historyLimit: resolvePromptHistoryLimit(
slack.historyLimit ?? cfg.messages?.groupChat?.historyLimit,
),
dmHistoryLimit: Math.max(0, slack.dmHistoryLimit ?? 0),
dmHistoryLimit: resolvePromptHistoryLimit(slack.dmHistoryLimit, 0),
sessionScope: cfg.session?.scope ?? ("per-sender" as const),
mainKey: normalizeMainKey(cfg.session?.mainKey),
dmEnabled: slack.dm?.enabled ?? true,

View file

@ -296,24 +296,31 @@ describe("automatic Telegram retained history workload", () => {
await state.cleanup();
});
it("reads only a physical window for the default 50 messages from a markerless archive", async () => {
const { runtime, cfg, telegramCfg } = historyRuntime();
const msg = archivedMessage(archiveSize + 1);
const context = await runtime.buildPromptContextForMessage(
{ message: msg, getFile: vi.fn() },
msg,
[],
cfg,
telegramCfg,
);
expect(telegramPromptContextHistory(context).map((entry) => entry.messageId)).toEqual(
Array.from({ length: 51 }, (_, index) => String(50_462 + index)).filter(
(id) => id !== "50500",
),
);
expect(reads.rows).toBeLessThanOrEqual(256);
expect(reads.pages).toBeLessThanOrEqual(2);
});
it.each([
{ historyLimit: undefined, limit: 50 },
{ historyLimit: Number.MAX_SAFE_INTEGER, limit: 50 },
{ historyLimit: 5000, limit: 200 },
])(
"bounds the automatic window to $limit for configured $historyLimit",
async ({ historyLimit, limit }) => {
const { runtime, cfg, telegramCfg } = historyRuntime({ groupPolicy: "open", historyLimit });
const msg = archivedMessage(archiveSize + 1);
const context = await runtime.buildPromptContextForMessage(
{ message: msg, getFile: vi.fn() },
msg,
[],
cfg,
telegramCfg,
);
expect(telegramPromptContextHistory(context).map((entry) => entry.messageId)).toEqual(
Array.from({ length: limit + 1 }, (_, index) => String(archiveSize - limit + index)).filter(
(id) => id !== "50500",
),
);
expect(reads.rows).toBeLessThanOrEqual(256);
expect(reads.pages).toBeLessThanOrEqual(2);
},
);
it("does not read storage when automatic history is zero and there is no reply", async () => {
const { runtime, cfg, telegramCfg } = historyRuntime({ groupPolicy: "open", historyLimit: 0 });

View file

@ -2,7 +2,7 @@ import type { Message } from "grammy/types";
import { formatMediaPlaceholderText } from "openclaw/plugin-sdk/channel-inbound";
import { resolveStoredModelOverride } from "openclaw/plugin-sdk/command-auth-native";
import type { OpenClawConfig, TelegramAccountConfig } from "openclaw/plugin-sdk/config-contracts";
import { DEFAULT_GROUP_HISTORY_LIMIT } from "openclaw/plugin-sdk/reply-history";
import { resolvePromptHistoryLimit } from "openclaw/plugin-sdk/number-runtime";
import {
getSessionEntry,
readAmbientTranscriptWatermark,
@ -457,11 +457,8 @@ export function createTelegramMessageContextRuntime({
return [];
}
const isGroup = msg.chat.type === "group" || msg.chat.type === "supergroup";
const groupHistoryLimit = Math.max(
0,
runtimeTelegramCfg.historyLimit ??
runtimeCfg.messages?.groupChat?.historyLimit ??
DEFAULT_GROUP_HISTORY_LIMIT,
const groupHistoryLimit = resolvePromptHistoryLimit(
runtimeTelegramCfg.historyLimit ?? runtimeCfg.messages?.groupChat?.historyLimit,
);
const dmHistoryLimit = resolveTelegramDmHistoryLimit({
config: runtimeTelegramCfg,

View file

@ -1,6 +1,6 @@
import type { OpenClawConfig, TelegramAccountConfig } from "openclaw/plugin-sdk/config-contracts";
import { resolvePromptHistoryLimit } from "openclaw/plugin-sdk/number-runtime";
import { resolveTextChunkLimit } from "openclaw/plugin-sdk/reply-chunking";
import { DEFAULT_GROUP_HISTORY_LIMIT } from "openclaw/plugin-sdk/reply-history";
import type { GetReplyOptions } from "openclaw/plugin-sdk/reply-runtime";
import {
createSubsystemLogger,
@ -103,11 +103,8 @@ export function resolveTelegramMessageTurnSettings(params: {
params.telegramCfg.groupAllowFrom ??
params.telegramCfg.allowFrom ??
allowFrom,
historyLimit: Math.max(
0,
params.telegramCfg.historyLimit ??
params.cfg.messages?.groupChat?.historyLimit ??
DEFAULT_GROUP_HISTORY_LIMIT,
historyLimit: resolvePromptHistoryLimit(
params.telegramCfg.historyLimit ?? params.cfg.messages?.groupChat?.historyLimit,
),
replyToMode: params.opts.replyToMode ?? params.telegramCfg.replyToMode ?? "off",
streamMode: resolveTelegramStreamMode(params.telegramCfg),

View file

@ -1,7 +1,20 @@
import { createChannelConfigUiHints } from "openclaw/plugin-sdk/channel-config-ui-hints";
import type { ChannelConfigUiHint } from "openclaw/plugin-sdk/channel-core";
const observedGroupHistoryHint = {
help: "Automatic observed-message context uses a default of 50 and a maximum of 200 messages; 0 disables automatic injection. The JSON integer maximum selects the 50-message default. Session transcript trimming separately counts user turns, where 0 means no trimming. The observed-message cap does not rewrite saved values.",
};
const observedDmHistoryHint = {
help: "Automatic observed-DM context uses a default of 10 and a maximum of 200 messages; 0 disables that extra context. The JSON integer maximum selects the 10-message default. Session transcript trimming separately counts user turns, where 0 means no trimming. The observed-message cap does not rewrite saved values.",
};
export const telegramChannelConfigUiHints = {
historyLimit: observedGroupHistoryHint,
"accounts.*.historyLimit": observedGroupHistoryHint,
dmHistoryLimit: observedDmHistoryHint,
"accounts.*.dmHistoryLimit": observedDmHistoryHint,
"dms.*.historyLimit": observedDmHistoryHint,
"accounts.*.dms.*.historyLimit": observedDmHistoryHint,
"": {
label: "Telegram",
help: "Telegram channel provider configuration including auth tokens, retry behavior, and message rendering controls. Use this section to tune bot behavior for Telegram-specific API semantics.",

View file

@ -9,6 +9,9 @@ describe("resolveTelegramDmHistoryLimit", () => {
it("honors and clamps the account limit", () => {
expect(resolveTelegramDmHistoryLimit({ config: { dmHistoryLimit: 4 } })).toBe(4);
expect(resolveTelegramDmHistoryLimit({ config: { dmHistoryLimit: -1 } })).toBe(0);
expect(
resolveTelegramDmHistoryLimit({ config: { dmHistoryLimit: Number.MAX_SAFE_INTEGER } }),
).toBe(10);
});
it("prefers the sender override over the account limit", () => {

View file

@ -1,5 +1,5 @@
// Telegram plugin module implements direct-message history limit resolution.
import type { TelegramAccountConfig } from "openclaw/plugin-sdk/config-contracts";
import { resolvePromptHistoryLimit } from "openclaw/plugin-sdk/number-runtime";
const DEFAULT_TELEGRAM_DM_HISTORY_LIMIT = 10;
@ -9,5 +9,8 @@ export function resolveTelegramDmHistoryLimit(params: {
}): number {
const senderId = params.senderId === undefined ? undefined : String(params.senderId);
const override = senderId ? params.config.dms?.[senderId]?.historyLimit : undefined;
return Math.max(0, override ?? params.config.dmHistoryLimit ?? DEFAULT_TELEGRAM_DM_HISTORY_LIMIT);
return resolvePromptHistoryLimit(
override ?? params.config.dmHistoryLimit,
DEFAULT_TELEGRAM_DM_HISTORY_LIMIT,
);
}

View file

@ -0,0 +1,61 @@
import type { OpenClawConfig, TelegramAccountConfig } from "openclaw/plugin-sdk/config-contracts";
import { resolvePromptHistoryLimit } from "openclaw/plugin-sdk/number-runtime";
import { describe, expect, it } from "vitest";
import { mergeTelegramAccountConfig } from "./account-config.js";
import { resolveTelegramDmHistoryLimit } from "./dm-history.js";
import { normalizeCompatibilityConfig } from "./doctor-contract.js";
const sentinel = Number.MAX_SAFE_INTEGER;
function effectiveLimits(cfg: OpenClawConfig, accountId?: string) {
const config: TelegramAccountConfig = accountId
? mergeTelegramAccountConfig(cfg, accountId)
: (cfg.channels?.telegram ?? {});
return [
resolvePromptHistoryLimit(config.historyLimit ?? cfg.messages?.groupChat?.historyLimit),
resolveTelegramDmHistoryLimit({ config }),
resolveTelegramDmHistoryLimit({ config, senderId: "42" }),
];
}
describe("Telegram observed history and saved limits", () => {
it.each([0, 7, 5000])("preserves sentinel overrides over inherited %s windows", (inherited) => {
const cfg: OpenClawConfig = {
messages: { groupChat: { historyLimit: inherited } },
channels: {
telegram: {
historyLimit: sentinel,
dmHistoryLimit: inherited,
dms: { "42": { historyLimit: sentinel } },
accounts: { work: { historyLimit: sentinel, dmHistoryLimit: sentinel } },
},
},
};
const before = [effectiveLimits(cfg), effectiveLimits(cfg, "work")];
const result = normalizeCompatibilityConfig({ cfg });
expect([effectiveLimits(result.config), effectiveLimits(result.config, "work")]).toEqual(
before,
);
expect(result.config).toEqual(cfg);
expect(result.changes).toEqual([]);
expect(cfg.channels?.telegram?.historyLimit).toBe(sentinel);
expect(normalizeCompatibilityConfig({ cfg: result.config }).changes).toEqual([]);
});
it("preserves ordinary and disabled account/sender windows", () => {
const cfg: OpenClawConfig = {
channels: {
telegram: {
historyLimit: 5000,
dmHistoryLimit: sentinel,
accounts: {
work: { historyLimit: 0, dmHistoryLimit: 7, dms: { "42": { historyLimit: 0 } } },
},
},
},
};
const result = normalizeCompatibilityConfig({ cfg });
expect(effectiveLimits(result.config)).toEqual([200, 10, 10]);
expect(effectiveLimits(result.config, "work")).toEqual([0, 7, 0]);
expect(result.config.channels?.telegram?.historyLimit).toBe(5000);
});
});

View file

@ -6,7 +6,7 @@ import { registerChannelRuntimeContext } from "openclaw/plugin-sdk/channel-runti
import { formatCliCommand } from "openclaw/plugin-sdk/cli-runtime";
import { drainPendingDeliveries } from "openclaw/plugin-sdk/delivery-queue-runtime";
import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
import { DEFAULT_GROUP_HISTORY_LIMIT } from "openclaw/plugin-sdk/reply-history";
import { resolvePromptHistoryLimit } from "openclaw/plugin-sdk/number-runtime";
import { resolveAgentRoute } from "openclaw/plugin-sdk/routing";
import {
registerUnhandledRejectionHandler,
@ -151,11 +151,11 @@ export async function monitorWebChannel(
const reconnectPolicy = resolveReconnectPolicy(cfg, tuning.reconnect);
const socketTiming = resolveWhatsAppSocketTiming(tuning.socketTiming);
const baseMentionConfig = buildMentionConfig(cfg);
const groupHistoryLimit =
const groupHistoryLimit = resolvePromptHistoryLimit(
account.historyLimit ??
cfg.channels?.whatsapp?.historyLimit ??
cfg.messages?.groupChat?.historyLimit ??
DEFAULT_GROUP_HISTORY_LIMIT;
cfg.channels?.whatsapp?.historyLimit ??
cfg.messages?.groupChat?.historyLimit,
);
const groupHistories = new Map<
string,
Array<{

View file

@ -24,11 +24,8 @@ import { isDangerousNameMatchingEnabled } from "openclaw/plugin-sdk/dangerous-na
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
import { createDeferred } from "openclaw/plugin-sdk/extension-shared";
import { channelReadyPatch } from "openclaw/plugin-sdk/gateway-runtime";
import {
DEFAULT_GROUP_HISTORY_LIMIT,
type HistoryEntry,
createChannelHistoryWindow,
} from "openclaw/plugin-sdk/reply-history";
import { resolvePromptHistoryLimit } from "openclaw/plugin-sdk/number-runtime";
import { type HistoryEntry, createChannelHistoryWindow } from "openclaw/plugin-sdk/reply-history";
import {
deliverTextOrMediaReply,
resolveSendableOutboundReplyParts,
@ -812,11 +809,8 @@ export async function monitorZalouserProvider(
});
const core = getZalouserRuntime();
const historyLimit = Math.max(
0,
account.config.historyLimit ??
config.messages?.groupChat?.historyLimit ??
DEFAULT_GROUP_HISTORY_LIMIT,
const historyLimit = resolvePromptHistoryLimit(
account.config.historyLimit ?? config.messages?.groupChat?.historyLimit,
);
const groupHistories = new Map<string, HistoryEntry[]>();

View file

@ -231,6 +231,7 @@ src/agents/worktrees/snapshot-inventory.ts
src/agents/worktrees/template-registry.ts
src/auto-reply/reply/delivery-hints.ts
src/auto-reply/reply/display-text-sanitize.ts
src/auto-reply/reply/history-limit.ts
src/auto-reply/reply/inbound-context-marker.ts
src/auto-reply/reply/strip-inbound-meta.ts
src/auto-reply/thinking.shared.ts

View file

@ -0,0 +1,84 @@
import { describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { applyPluginDoctorCompatibilityMigrations } from "../../plugins/doctor-contract-registry.js";
import type { AgentMessage } from "../runtime/index.js";
import { getHistoryLimitFromSessionKey, limitHistoryTurns } from "./history.js";
const sentinel = Number.MAX_SAFE_INTEGER;
function normalizeCompatibilityConfig({ cfg }: { cfg: OpenClawConfig }) {
return applyPluginDoctorCompatibilityMigrations(cfg, { pluginIds: ["telegram"] });
}
function transcript(turns: number): AgentMessage[] {
return Array.from({ length: turns }, (_, index) => ({
role: "user",
content: "turn " + index,
timestamp: index,
}));
}
function retained(cfg: OpenClawConfig, key: string, messages: AgentMessage[], accountId?: string) {
return limitHistoryTurns(messages, getHistoryLimitFromSessionKey(key, cfg, { accountId }));
}
describe("Doctor preserves independent session transcript limits", () => {
it.each([0, 7, 5000])("preserves actual contents with inherited limit %s", (inherited) => {
const cfg: OpenClawConfig = {
messages: { groupChat: { historyLimit: sentinel } },
channels: {
telegram: {
historyLimit: sentinel,
dmHistoryLimit: inherited,
dms: { "42": { historyLimit: sentinel } },
accounts: {
work: {
historyLimit: sentinel,
dmHistoryLimit: sentinel,
dms: { "44": { historyLimit: sentinel } },
},
},
},
},
};
const routes = [
{ key: "agent:main:telegram:direct:42", turns: 16 },
{ key: "agent:main:telegram:direct:99", turns: 16 },
{ key: "agent:main:telegram:work:direct:42", turns: 16, accountId: "work" },
{ key: "agent:main:telegram:work:direct:44", turns: 16, accountId: "work" },
{ key: "agent:main:telegram:group:-100", turns: 76 },
{ key: "agent:main:telegram:group:-100", turns: 76, accountId: "work" },
];
const result = normalizeCompatibilityConfig({ cfg });
for (const route of routes) {
const messages = transcript(route.turns);
expect(retained(result.config, route.key, messages, route.accountId)).toEqual(
retained(cfg, route.key, messages, route.accountId),
);
}
expect(result.config).toEqual(cfg);
expect(result.changes).toEqual([]);
expect(normalizeCompatibilityConfig({ cfg: result.config }).config).toEqual(cfg);
});
it("retains the raw inherited limit when an existing retired-mode migration materializes it", () => {
// Doctor accepts retired fields that are deliberately absent from the current schema.
const cfg = {
channels: {
telegram: {
historyLimit: sentinel,
includeGroupHistoryContext: "none",
accounts: { work: { includeGroupHistoryContext: "recent" } },
},
},
} as unknown as OpenClawConfig;
const messages = transcript(76);
const key = "agent:main:telegram:group:-100";
const before = retained(cfg, key, messages, "work");
const result = normalizeCompatibilityConfig({ cfg });
expect(before).toEqual(messages);
expect(retained(result.config, key, messages, "work")).toEqual(before);
expect(result.config.channels?.telegram?.accounts?.work?.historyLimit).toBe(sentinel);
expect(result.config.channels?.telegram?.historyLimit).toBe(0);
expect(normalizeCompatibilityConfig({ cfg: result.config }).changes).toEqual([]);
});
});

View file

@ -9,7 +9,7 @@ import {
} from "../../gateway/agent-runtime-identity-token.js";
import { resolveExecutionIdentitySpawnFacts } from "../../gateway/agent-turn/agent-run-execution-lineage.js";
import type { CallGatewayOptions } from "../../gateway/call.js";
import { createTestApprovalManager } from "../../gateway/exec-approval-manager.test-support.js";
import { createPreparedTestApprovalManager } from "../../gateway/exec-approval-manager.test-support.js";
import {
mintMessageActionTurnCapability,
revokeMessageActionTurnCapability,
@ -722,7 +722,7 @@ describe("gateway tool runtime identity", () => {
"rejects late $method registration after permission change (ambient lifetime: $ambient)",
async ({ method, ambient }, testContext) => {
mocks.callGateway.mockResolvedValue({ id: "approval" });
const manager = createTestApprovalManager<{
const { manager } = await createPreparedTestApprovalManager<{
command: string;
title: string;
description: string;

View file

@ -1,6 +1,7 @@
/** Appends channel-supplied prompt context to the user-role body under a marked label. */
import { truncateUtf16Safe } from "../../utils.js";
import type { TemplateContext } from "../templating.js";
import { resolvePromptHistoryLimit } from "./history-limit.js";
import { markInboundContextLabel } from "./inbound-context-marker.js";
import { normalizeInboundTextNewlines } from "./inbound-text.js";
@ -69,7 +70,7 @@ export function selectInboundHistoryContext(
typeof configuredLimit === "number" &&
Number.isSafeInteger(configuredLimit) &&
configuredLimit >= 0
? configuredLimit
? resolvePromptHistoryLimit(configuredLimit, 20)
: 20;
return {
boundedHistory: limit > 0 ? history.slice(-limit) : [],

View file

@ -0,0 +1,22 @@
// Shared numeric policy for automatic observed-message context, not session transcript turns.
export const DEFAULT_GROUP_HISTORY_LIMIT = 50;
/** Hard cap for prompt-injected history windows. JSON-schema integer maximum is not a window. */
const MAX_PROMPT_HISTORY_LIMIT = 200;
/** Resolves one bounded observed-message window without rewriting saved configuration. */
export function resolvePromptHistoryLimit(
configured: unknown,
fallback: number = DEFAULT_GROUP_HISTORY_LIMIT,
): number {
const isSchemaMaximum =
typeof configured === "number" &&
Number.isInteger(configured) &&
configured >= Number.MAX_SAFE_INTEGER;
const selected =
typeof configured === "number" && Number.isFinite(configured) && !isSchemaMaximum
? configured
: fallback;
return Number.isFinite(selected)
? Math.min(Math.max(0, Math.trunc(selected)), MAX_PROMPT_HISTORY_LIMIT)
: 0;
}

View file

@ -4,7 +4,7 @@ import type { HistoryEntry, HistoryMediaEntry } from "./history.types.js";
export const HISTORY_CONTEXT_MARKER = "[Chat messages since your last reply - for context]";
export const RECENT_HISTORY_CONTEXT_MARKER = "[Recent chat messages - for context]";
export const CURRENT_MESSAGE_MARKER = "[Current message - respond to this]";
export const DEFAULT_GROUP_HISTORY_LIMIT = 50;
export { DEFAULT_GROUP_HISTORY_LIMIT } from "./history-limit.js";
/** Maximum number of group history keys to retain (LRU eviction when exceeded). */
const MAX_HISTORY_KEYS = 1000;

View file

@ -0,0 +1,18 @@
import { describe, expect, it } from "vitest";
import { LEGACY_CONFIG_MIGRATIONS_CHANNELS } from "./legacy-config-migrations.channels.js";
describe("shared group history configuration", () => {
it.each([Number.MAX_SAFE_INTEGER, 0, 7, 5000])(
"preserves the valid saved limit %s",
(historyLimit) => {
const raw = { messages: { groupChat: { historyLimit, visibleReplies: "automatic" } } };
const before = structuredClone(raw);
const changes: string[] = [];
for (const migration of LEGACY_CONFIG_MIGRATIONS_CHANNELS) {
migration.apply(raw, changes);
}
expect(raw).toEqual(before);
expect(changes).toEqual([]);
},
);
});

File diff suppressed because one or more lines are too long

View file

@ -48,6 +48,19 @@ describe("applySharedChannelFieldHelp", () => {
);
});
it("does not impose message-window semantics on shared transcript limits", () => {
const next = applySharedChannelFieldHelp({
"channels.signal.dmHistoryLimit": {},
"channels.msteams.accounts.*.dmHistoryLimit": {},
"channels.signal.historyLimit": {},
});
for (const hint of Object.values(next)) {
expect(hint.help).toContain("user-turn");
expect(hint.help).not.toContain("200");
expect(hint.help).not.toContain("0 disables");
}
});
it("treats empty help as a deliberate suppression, not a missing value", () => {
const next = applySharedChannelFieldHelp({
"channels.irc.enabled": { advanced: false, help: "" },

View file

@ -24,7 +24,8 @@ const SHARED_CHANNEL_FIELD_HELP: Record<string, string> = {
defaultAccount: "Account used when an outbound request does not name one.",
defaultTo: "Target used for outbound messages when the caller supplies none.",
dm: "Settings that apply only to direct messages on this channel.",
dmHistoryLimit: "History limit for direct messages. Overrides the channel history limit in DMs.",
dmHistoryLimit:
"Channel-specific direct-message history limit. Consult channel guidance for message-window defaults and session user-turn trimming; zero has different meanings for those consumers.",
dmPolicy:
'Who may DM the agent: "pairing" approves each new sender, "allowlist" trusts allowFrom, "open" allows anyone, "disabled" turns DMs off.',
dms: "Per-conversation overrides, keyed by DM id.",
@ -37,7 +38,8 @@ const SHARED_CHANNEL_FIELD_HELP: Record<string, string> = {
groups: "Per-group overrides, keyed by group id.",
healthMonitor: "Per-channel opt-out for the health monitor that restarts stalled channels.",
heartbeatVisibility: "Which heartbeat results this channel shows.",
historyLimit: "How many earlier messages to include as context. 0 disables history.",
historyLimit:
"Channel-specific history limit. Consult channel guidance for observed-message windows and session user-turn trimming; zero has different meanings for those consumers.",
markdown: "Markdown rendering overrides for this channel.",
mediaMaxMb: "Per-attachment media limit in MiB. Channel transport limits still apply.",
mentionPatterns: "Extra patterns that count as mentioning the agent in group chats.",

View file

@ -274,7 +274,7 @@ export const AUTOMATION_FIELD_HELP: Record<string, string> = {
"messages.groupChat.mentionPatterns":
"Safe case-insensitive regex patterns used to detect explicit mentions/trigger phrases in group chats. Use precise patterns to reduce false positives in high-volume channels; invalid or unsafe nested-repetition patterns are ignored.",
"messages.groupChat.historyLimit":
"Maximum number of prior group messages loaded as context per turn for group sessions. Use higher values for richer continuity, or lower values for faster and cheaper responses.",
"Maximum number of prior group messages loaded as context per turn for group sessions. Use higher values for richer continuity, or lower values for faster and cheaper responses. Automatic prompt history is capped at 200 messages. The JSON integer maximum selects the channel default window (50 for shared group history), not an unlimited window.",
"messages.groupChat.unmentionedInbound":
'Controls how unmentioned always-on group chatter is submitted. "user_request" treats it as a user request; "room_event" submits it as quiet context where visible output requires the message tool.',
"messages.groupChat.visibleReplies":

View file

@ -0,0 +1,19 @@
import { describe, expect, it } from "vitest";
import { resolvePromptHistoryLimit } from "./number-runtime.js";
describe("private observed-message history policy", () => {
it("applies channel defaults, explicit disablement, and the message ceiling", () => {
expect(resolvePromptHistoryLimit(undefined)).toBe(50);
expect(resolvePromptHistoryLimit(12)).toBe(12);
expect(resolvePromptHistoryLimit(Number.MAX_SAFE_INTEGER)).toBe(50);
expect(resolvePromptHistoryLimit(Number.MAX_SAFE_INTEGER, 10)).toBe(10);
expect(resolvePromptHistoryLimit(0, 10)).toBe(0);
expect(resolvePromptHistoryLimit(5000, 10)).toBe(200);
});
it.each([undefined, Number.MAX_SAFE_INTEGER])("bounds fallback windows for %s", (configured) => {
expect(resolvePromptHistoryLimit(configured, 5000)).toBe(200);
expect(resolvePromptHistoryLimit(configured, -1)).toBe(0);
expect(resolvePromptHistoryLimit(configured, Number.POSITIVE_INFINITY)).toBe(0);
});
});

View file

@ -39,3 +39,6 @@ export {
resolveExpiresAtMsFromEpochSeconds,
} from "../../packages/normalization-core/src/number-coercion.js";
export { MAX_TCP_PORT, parseTcpPort } from "../infra/tcp-port.js";
// Private observed-message policy shared by official channel implementations.
export { resolvePromptHistoryLimit } from "../auto-reply/reply/history-limit.js";