2026-02-10 15:33:57 -08:00
|
|
|
import {
|
|
|
|
|
resolveChannelGroupRequireMention,
|
|
|
|
|
resolveChannelGroupToolsPolicy,
|
|
|
|
|
} from "../config/group-policy.js";
|
2026-03-05 23:07:13 -06:00
|
|
|
import { inspectDiscordAccount } from "../discord/account-inspect.js";
|
2026-03-02 08:53:11 +00:00
|
|
|
import {
|
|
|
|
|
formatTrimmedAllowFromEntries,
|
|
|
|
|
formatWhatsAppConfigAllowFromEntries,
|
|
|
|
|
resolveIMessageConfigAllowFrom,
|
|
|
|
|
resolveIMessageConfigDefaultTo,
|
|
|
|
|
resolveWhatsAppConfigAllowFrom,
|
|
|
|
|
resolveWhatsAppConfigDefaultTo,
|
|
|
|
|
} from "../plugin-sdk/channel-config-helpers.js";
|
2026-02-01 10:03:47 +09:00
|
|
|
import { requireActivePluginRegistry } from "../plugins/runtime.js";
|
|
|
|
|
import { normalizeAccountId } from "../routing/session-key.js";
|
2026-01-11 11:45:25 +00:00
|
|
|
import { resolveSignalAccount } from "../signal/accounts.js";
|
2026-03-05 23:07:13 -06:00
|
|
|
import { inspectSlackAccount } from "../slack/account-inspect.js";
|
|
|
|
|
import { resolveSlackReplyToMode } from "../slack/accounts.js";
|
2026-01-21 20:01:12 +00:00
|
|
|
import { buildSlackThreadingToolContext } from "../slack/threading-tool-context.js";
|
2026-03-05 23:07:13 -06:00
|
|
|
import { inspectTelegramAccount } from "../telegram/account-inspect.js";
|
2026-02-23 21:25:20 +00:00
|
|
|
import { normalizeE164 } from "../utils.js";
|
2026-01-11 11:45:25 +00:00
|
|
|
import {
|
|
|
|
|
resolveDiscordGroupRequireMention,
|
2026-01-24 15:35:05 +13:00
|
|
|
resolveDiscordGroupToolPolicy,
|
2026-01-23 16:45:37 -06:00
|
|
|
resolveGoogleChatGroupRequireMention,
|
|
|
|
|
resolveGoogleChatGroupToolPolicy,
|
2026-01-11 11:45:25 +00:00
|
|
|
resolveIMessageGroupRequireMention,
|
2026-01-24 15:35:05 +13:00
|
|
|
resolveIMessageGroupToolPolicy,
|
Fix/Complete LINE `requireMention` gating behavior (#35847)
* fix(line): enforce requireMention gating in group message handler
* fix(line): scope canDetectMention to text messages, pass hasAnyMention
* fix(line): fix TS errors in mentionees type and test casts
* feat(line): register LINE in DOCKS and CHAT_CHANNEL_ORDER
- Add "line" to CHAT_CHANNEL_ORDER and CHAT_CHANNEL_META in registry.ts
- Export resolveLineGroupRequireMention and resolveLineGroupToolPolicy
in group-mentions.ts using the generic resolveChannelGroupRequireMention
and resolveChannelGroupToolsPolicy helpers (same pattern as iMessage)
- Add "line" entry to DOCKS in dock.ts so resolveGroupRequireMention
in the reply stage can correctly read LINE group config
Fixes the third layer of the requireMention bug: previously
getChannelDock("line") returned undefined, causing the reply-stage
resolveGroupRequireMention to fall back to true unconditionally.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(line): pending history, requireMention default, mentionPatterns fallback
- Default requireMention to true (consistent with other channels)
- Add mentionPatterns regex fallback alongside native isSelf/@all detection
- Record unmentioned group messages via recordPendingHistoryEntryIfEnabled
- Inject pending history context in buildLineMessageContext when bot is mentioned
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(line): update tests for requireMention default and pending history
- Add requireMention: false to 6 group tests unrelated to mention gating
(allowlist, replay dedup, inflight dedup, error retry) to preserve
their original intent after the default changed from false to true
- Add test: skips group messages by default when requireMention not configured
- Add test: records unmentioned group messages as pending history
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(line): use undefined instead of empty string as historyKey sentinel
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(line): deliver pending history via InboundHistory, not Body mutation
- Remove post-hoc ctxPayload.Body injection (BodyForAgent takes priority
in the prompt pipeline, so Body was never reached)
- Pass InboundHistory array to finalizeInboundContext instead, matching
the Telegram pattern rendered by buildInboundUserContextPrefix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(line): pass agentId to buildMentionRegexes for per-agent mentionPatterns
- Resolve route before mention gating to obtain agentId
- Pass agentId to buildMentionRegexes, matching Telegram behavior
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(line): clear pending history after handled group turn
- Call clearHistoryEntriesIfEnabled after processMessage for group messages
- Prevents stale skipped messages from replaying on subsequent mentions
- Matches Discord, Signal, Slack, iMessage behavior
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* style(line): fix import order and merge orphaned JSDoc in bot-handlers
- Move resolveAgentRoute import from ./local group to ../routing group
- Merge duplicate JSDoc blocks above getLineMentionees into one
Addresses Greptile review comments r2888826724 and r2888826840 on PR #35847.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(line): read historyLimit from config and guard clear with has()
- bot.ts: resolve historyLimit from cfg.messages.groupChat.historyLimit
with fallback to DEFAULT_GROUP_HISTORY_LIMIT, so setting historyLimit: 0
actually disables pending history accumulation
- bot-handlers.ts: add groupHistories.has(historyKey) guard before
clearHistoryEntriesIfEnabled to prevent writing empty buckets for
groups that have never accumulated pending history (memory leak)
Addresses Codex review comments r2888829146 and r2888829152 on PR #35847.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* style(line): apply oxfmt formatting to bot-handlers and bot
Auto-formatted by oxfmt to fix CI format:check failure on PR #35847.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(line): add shouldLogVerbose to globals mock in bot-handlers test
resolveAgentRoute calls shouldLogVerbose() from globals.js; the mock
was missing this export, causing 13 test failures.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Address review findings for #35847
---------
Co-authored-by: Kaiyi <me@kaiyi.cool>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Yi-Cheng Wang <yicheng.wang@heph-ai.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
2026-03-08 04:06:07 +08:00
|
|
|
resolveLineGroupRequireMention,
|
|
|
|
|
resolveLineGroupToolPolicy,
|
2026-01-11 11:45:25 +00:00
|
|
|
resolveSlackGroupRequireMention,
|
2026-01-24 15:35:05 +13:00
|
|
|
resolveSlackGroupToolPolicy,
|
2026-01-11 11:45:25 +00:00
|
|
|
resolveTelegramGroupRequireMention,
|
2026-01-24 15:35:05 +13:00
|
|
|
resolveTelegramGroupToolPolicy,
|
2026-01-11 11:45:25 +00:00
|
|
|
resolveWhatsAppGroupRequireMention,
|
2026-01-24 15:35:05 +13:00
|
|
|
resolveWhatsAppGroupToolPolicy,
|
2026-01-11 11:45:25 +00:00
|
|
|
} from "./plugins/group-mentions.js";
|
2026-02-17 14:17:22 -08:00
|
|
|
import { normalizeSignalMessagingTarget } from "./plugins/normalize/signal.js";
|
2026-02-18 01:34:35 +00:00
|
|
|
import type {
|
|
|
|
|
ChannelCapabilities,
|
|
|
|
|
ChannelCommandAdapter,
|
2026-02-22 07:37:54 +00:00
|
|
|
ChannelConfigAdapter,
|
2026-02-18 01:34:35 +00:00
|
|
|
ChannelElevatedAdapter,
|
|
|
|
|
ChannelGroupAdapter,
|
|
|
|
|
ChannelId,
|
|
|
|
|
ChannelAgentPromptAdapter,
|
|
|
|
|
ChannelMentionAdapter,
|
|
|
|
|
ChannelPlugin,
|
|
|
|
|
ChannelThreadingContext,
|
|
|
|
|
ChannelThreadingAdapter,
|
|
|
|
|
ChannelThreadingToolContext,
|
|
|
|
|
} from "./plugins/types.js";
|
2026-02-23 21:25:20 +00:00
|
|
|
import {
|
|
|
|
|
resolveWhatsAppGroupIntroHint,
|
|
|
|
|
resolveWhatsAppMentionStripPatterns,
|
|
|
|
|
} from "./plugins/whatsapp-shared.js";
|
2026-01-15 02:42:41 +00:00
|
|
|
import { CHAT_CHANNEL_ORDER, type ChatChannelId, getChatChannelMeta } from "./registry.js";
|
2026-01-11 11:45:25 +00:00
|
|
|
|
2026-01-13 06:16:43 +00:00
|
|
|
export type ChannelDock = {
|
|
|
|
|
id: ChannelId;
|
|
|
|
|
capabilities: ChannelCapabilities;
|
|
|
|
|
commands?: ChannelCommandAdapter;
|
2026-01-11 11:45:25 +00:00
|
|
|
outbound?: {
|
|
|
|
|
textChunkLimit?: number;
|
|
|
|
|
};
|
2026-01-13 06:16:43 +00:00
|
|
|
streaming?: ChannelDockStreaming;
|
|
|
|
|
elevated?: ChannelElevatedAdapter;
|
2026-02-22 07:37:54 +00:00
|
|
|
config?: Pick<
|
|
|
|
|
ChannelConfigAdapter<unknown>,
|
|
|
|
|
"resolveAllowFrom" | "formatAllowFrom" | "resolveDefaultTo"
|
|
|
|
|
>;
|
2026-01-13 06:16:43 +00:00
|
|
|
groups?: ChannelGroupAdapter;
|
|
|
|
|
mentions?: ChannelMentionAdapter;
|
|
|
|
|
threading?: ChannelThreadingAdapter;
|
2026-01-22 03:27:26 +00:00
|
|
|
agentPrompt?: ChannelAgentPromptAdapter;
|
2026-01-11 11:45:25 +00:00
|
|
|
};
|
|
|
|
|
|
2026-01-13 06:16:43 +00:00
|
|
|
type ChannelDockStreaming = {
|
2026-01-11 11:45:25 +00:00
|
|
|
blockStreamingCoalesceDefaults?: {
|
|
|
|
|
minChars?: number;
|
|
|
|
|
idleMs?: number;
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const formatLower = (allowFrom: Array<string | number>) =>
|
|
|
|
|
allowFrom
|
|
|
|
|
.map((entry) => String(entry).trim())
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
.map((entry) => entry.toLowerCase());
|
2026-02-15 18:30:34 +00:00
|
|
|
|
2026-02-22 07:37:54 +00:00
|
|
|
const stringifyAllowFrom = (allowFrom: Array<string | number>) =>
|
|
|
|
|
allowFrom.map((entry) => String(entry));
|
|
|
|
|
|
|
|
|
|
const trimAllowFromEntries = (allowFrom: Array<string | number>) =>
|
|
|
|
|
allowFrom.map((entry) => String(entry).trim()).filter(Boolean);
|
|
|
|
|
|
2026-02-22 14:05:46 +00:00
|
|
|
const DEFAULT_OUTBOUND_TEXT_CHUNK_LIMIT_4000 = { textChunkLimit: 4000 };
|
|
|
|
|
|
|
|
|
|
const DEFAULT_BLOCK_STREAMING_COALESCE = {
|
|
|
|
|
blockStreamingCoalesceDefaults: { minChars: 1500, idleMs: 1000 },
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function formatAllowFromWithReplacements(
|
|
|
|
|
allowFrom: Array<string | number>,
|
|
|
|
|
replacements: RegExp[],
|
|
|
|
|
): string[] {
|
|
|
|
|
return trimAllowFromEntries(allowFrom).map((entry) => {
|
|
|
|
|
let normalized = entry;
|
|
|
|
|
for (const replacement of replacements) {
|
|
|
|
|
normalized = normalized.replace(replacement, "");
|
|
|
|
|
}
|
|
|
|
|
return normalized.toLowerCase();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 08:45:12 -05:00
|
|
|
const formatDiscordAllowFrom = (allowFrom: Array<string | number>) =>
|
|
|
|
|
allowFrom
|
|
|
|
|
.map((entry) =>
|
|
|
|
|
String(entry)
|
|
|
|
|
.trim()
|
|
|
|
|
.replace(/^<@!?/, "")
|
|
|
|
|
.replace(/>$/, "")
|
|
|
|
|
.replace(/^discord:/i, "")
|
|
|
|
|
.replace(/^user:/i, "")
|
|
|
|
|
.replace(/^pk:/i, "")
|
|
|
|
|
.trim()
|
|
|
|
|
.toLowerCase(),
|
|
|
|
|
)
|
|
|
|
|
.filter(Boolean);
|
|
|
|
|
|
2026-02-17 14:17:22 -08:00
|
|
|
function resolveDirectOrGroupChannelId(context: ChannelThreadingContext): string | undefined {
|
|
|
|
|
const isDirect = context.ChatType?.toLowerCase() === "direct";
|
|
|
|
|
return (isDirect ? (context.From ?? context.To) : context.To)?.trim() || undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildSignalThreadToolContext(params: {
|
|
|
|
|
context: ChannelThreadingContext;
|
|
|
|
|
hasRepliedRef: ChannelThreadingToolContext["hasRepliedRef"];
|
|
|
|
|
}): ChannelThreadingToolContext {
|
|
|
|
|
const currentChannelIdRaw = resolveDirectOrGroupChannelId(params.context);
|
|
|
|
|
const currentChannelId = currentChannelIdRaw
|
|
|
|
|
? (normalizeSignalMessagingTarget(currentChannelIdRaw) ?? currentChannelIdRaw.trim())
|
|
|
|
|
: undefined;
|
|
|
|
|
return {
|
|
|
|
|
currentChannelId,
|
|
|
|
|
currentThreadTs: params.context.ReplyToId,
|
|
|
|
|
hasRepliedRef: params.hasRepliedRef,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildIMessageThreadToolContext(params: {
|
2026-02-15 18:30:34 +00:00
|
|
|
context: ChannelThreadingContext;
|
|
|
|
|
hasRepliedRef: ChannelThreadingToolContext["hasRepliedRef"];
|
|
|
|
|
}): ChannelThreadingToolContext {
|
|
|
|
|
return {
|
2026-02-17 14:17:22 -08:00
|
|
|
currentChannelId: resolveDirectOrGroupChannelId(params.context),
|
2026-02-15 18:30:34 +00:00
|
|
|
currentThreadTs: params.context.ReplyToId,
|
|
|
|
|
hasRepliedRef: params.hasRepliedRef,
|
|
|
|
|
};
|
|
|
|
|
}
|
2026-02-18 19:04:57 +00:00
|
|
|
|
2026-02-22 07:37:54 +00:00
|
|
|
function buildThreadToolContextFromMessageThreadOrReply(params: {
|
|
|
|
|
context: ChannelThreadingContext;
|
|
|
|
|
hasRepliedRef: ChannelThreadingToolContext["hasRepliedRef"];
|
|
|
|
|
}): ChannelThreadingToolContext {
|
|
|
|
|
const threadId = params.context.MessageThreadId ?? params.context.ReplyToId;
|
|
|
|
|
return {
|
|
|
|
|
currentChannelId: params.context.To?.trim() || undefined,
|
|
|
|
|
currentThreadTs: threadId != null ? String(threadId) : undefined,
|
|
|
|
|
hasRepliedRef: params.hasRepliedRef,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-18 19:04:57 +00:00
|
|
|
function resolveCaseInsensitiveAccount<T>(
|
|
|
|
|
accounts: Record<string, T> | undefined,
|
|
|
|
|
accountId?: string | null,
|
|
|
|
|
): T | undefined {
|
|
|
|
|
if (!accounts) {
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
const normalized = normalizeAccountId(accountId);
|
|
|
|
|
return (
|
|
|
|
|
accounts[normalized] ??
|
|
|
|
|
accounts[
|
|
|
|
|
Object.keys(accounts).find((key) => key.toLowerCase() === normalized.toLowerCase()) ?? ""
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-02-21 23:56:58 +00:00
|
|
|
|
|
|
|
|
function resolveDefaultToCaseInsensitiveAccount(params: {
|
|
|
|
|
channel?:
|
|
|
|
|
| {
|
|
|
|
|
accounts?: Record<string, { defaultTo?: string }>;
|
|
|
|
|
defaultTo?: string;
|
|
|
|
|
}
|
|
|
|
|
| undefined;
|
|
|
|
|
accountId?: string | null;
|
|
|
|
|
}): string | undefined {
|
|
|
|
|
const account = resolveCaseInsensitiveAccount(params.channel?.accounts, params.accountId);
|
|
|
|
|
return (account?.defaultTo ?? params.channel?.defaultTo)?.trim() || undefined;
|
|
|
|
|
}
|
2026-02-22 14:05:46 +00:00
|
|
|
|
|
|
|
|
function resolveChannelDefaultTo(
|
|
|
|
|
channel:
|
|
|
|
|
| {
|
|
|
|
|
accounts?: Record<string, { defaultTo?: string }>;
|
|
|
|
|
defaultTo?: string;
|
|
|
|
|
}
|
|
|
|
|
| undefined,
|
|
|
|
|
accountId?: string | null,
|
|
|
|
|
): string | undefined {
|
|
|
|
|
return resolveDefaultToCaseInsensitiveAccount({ channel, accountId });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type CaseInsensitiveDefaultToChannel = {
|
|
|
|
|
accounts?: Record<string, { defaultTo?: string }>;
|
|
|
|
|
defaultTo?: string;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type CaseInsensitiveDefaultToChannels = Partial<
|
|
|
|
|
Record<"irc" | "googlechat", CaseInsensitiveDefaultToChannel>
|
|
|
|
|
>;
|
|
|
|
|
|
|
|
|
|
function resolveNamedChannelDefaultTo(params: {
|
|
|
|
|
channels?: CaseInsensitiveDefaultToChannels;
|
|
|
|
|
channelId: keyof CaseInsensitiveDefaultToChannels;
|
|
|
|
|
accountId?: string | null;
|
|
|
|
|
}): string | undefined {
|
|
|
|
|
return resolveChannelDefaultTo(params.channels?.[params.channelId], params.accountId);
|
|
|
|
|
}
|
2026-01-13 06:16:43 +00:00
|
|
|
// Channel docks: lightweight channel metadata/behavior for shared code paths.
|
2026-01-11 11:45:25 +00:00
|
|
|
//
|
|
|
|
|
// Rules:
|
|
|
|
|
// - keep this module *light* (no monitors, probes, puppeteer/web login, etc)
|
|
|
|
|
// - OK: config readers, allowFrom formatting, mention stripping patterns, threading defaults
|
2026-01-13 06:16:43 +00:00
|
|
|
// - shared code should import from here (and from `src/channels/registry.ts`), not from the plugins registry
|
2026-01-11 11:45:25 +00:00
|
|
|
//
|
2026-01-13 06:16:43 +00:00
|
|
|
// Adding a channel:
|
2026-01-11 11:45:25 +00:00
|
|
|
// - add a new entry to `DOCKS`
|
2026-01-13 06:16:43 +00:00
|
|
|
// - keep it cheap; push heavy logic into `src/channels/plugins/<id>.ts` or channel modules
|
2026-01-15 02:42:41 +00:00
|
|
|
const DOCKS: Record<ChatChannelId, ChannelDock> = {
|
2026-01-11 11:45:25 +00:00
|
|
|
telegram: {
|
|
|
|
|
id: "telegram",
|
|
|
|
|
capabilities: {
|
|
|
|
|
chatTypes: ["direct", "group", "channel", "thread"],
|
|
|
|
|
nativeCommands: true,
|
|
|
|
|
blockStreaming: true,
|
|
|
|
|
},
|
2026-02-22 14:05:46 +00:00
|
|
|
outbound: DEFAULT_OUTBOUND_TEXT_CHUNK_LIMIT_4000,
|
2026-01-11 11:45:25 +00:00
|
|
|
config: {
|
|
|
|
|
resolveAllowFrom: ({ cfg, accountId }) =>
|
2026-03-05 23:07:13 -06:00
|
|
|
stringifyAllowFrom(inspectTelegramAccount({ cfg, accountId }).config.allowFrom ?? []),
|
2026-01-11 11:45:25 +00:00
|
|
|
formatAllowFrom: ({ allowFrom }) =>
|
2026-02-22 07:37:54 +00:00
|
|
|
trimAllowFromEntries(allowFrom)
|
2026-01-11 11:45:25 +00:00
|
|
|
.map((entry) => entry.replace(/^(telegram|tg):/i, ""))
|
|
|
|
|
.map((entry) => entry.toLowerCase()),
|
2026-02-19 23:37:19 -05:00
|
|
|
resolveDefaultTo: ({ cfg, accountId }) => {
|
2026-03-05 23:07:13 -06:00
|
|
|
const val = inspectTelegramAccount({ cfg, accountId }).config.defaultTo;
|
2026-02-19 23:37:19 -05:00
|
|
|
return val != null ? String(val) : undefined;
|
|
|
|
|
},
|
2026-01-11 11:45:25 +00:00
|
|
|
},
|
|
|
|
|
groups: {
|
|
|
|
|
resolveRequireMention: resolveTelegramGroupRequireMention,
|
2026-01-24 15:35:05 +13:00
|
|
|
resolveToolPolicy: resolveTelegramGroupToolPolicy,
|
2026-01-11 11:45:25 +00:00
|
|
|
},
|
|
|
|
|
threading: {
|
2026-02-13 22:21:08 -08:00
|
|
|
resolveReplyToMode: ({ cfg }) => cfg.channels?.telegram?.replyToMode ?? "off",
|
2026-02-23 23:25:14 +08:00
|
|
|
buildToolContext: ({ context, hasRepliedRef }) => {
|
|
|
|
|
// Telegram auto-threading should only use actual thread/topic IDs.
|
|
|
|
|
// ReplyToId is a message ID and causes invalid message_thread_id in DMs.
|
|
|
|
|
const threadId = context.MessageThreadId;
|
|
|
|
|
const rawCurrentMessageId = context.CurrentMessageId;
|
|
|
|
|
const currentMessageId =
|
|
|
|
|
typeof rawCurrentMessageId === "number"
|
|
|
|
|
? rawCurrentMessageId
|
|
|
|
|
: rawCurrentMessageId?.trim() || undefined;
|
|
|
|
|
return {
|
|
|
|
|
currentChannelId: context.To?.trim() || undefined,
|
|
|
|
|
currentThreadTs: threadId != null ? String(threadId) : undefined,
|
|
|
|
|
currentMessageId,
|
|
|
|
|
hasRepliedRef,
|
|
|
|
|
};
|
|
|
|
|
},
|
2026-01-11 11:45:25 +00:00
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
whatsapp: {
|
|
|
|
|
id: "whatsapp",
|
|
|
|
|
capabilities: {
|
|
|
|
|
chatTypes: ["direct", "group"],
|
|
|
|
|
polls: true,
|
|
|
|
|
reactions: true,
|
|
|
|
|
media: true,
|
|
|
|
|
},
|
|
|
|
|
commands: {
|
|
|
|
|
enforceOwnerForCommands: true,
|
|
|
|
|
skipWhenConfigEmpty: true,
|
|
|
|
|
},
|
2026-02-22 14:05:46 +00:00
|
|
|
outbound: DEFAULT_OUTBOUND_TEXT_CHUNK_LIMIT_4000,
|
2026-01-11 11:45:25 +00:00
|
|
|
config: {
|
2026-03-02 08:53:11 +00:00
|
|
|
resolveAllowFrom: ({ cfg, accountId }) => resolveWhatsAppConfigAllowFrom({ cfg, accountId }),
|
|
|
|
|
formatAllowFrom: ({ allowFrom }) => formatWhatsAppConfigAllowFromEntries(allowFrom),
|
|
|
|
|
resolveDefaultTo: ({ cfg, accountId }) => resolveWhatsAppConfigDefaultTo({ cfg, accountId }),
|
2026-01-11 11:45:25 +00:00
|
|
|
},
|
|
|
|
|
groups: {
|
|
|
|
|
resolveRequireMention: resolveWhatsAppGroupRequireMention,
|
2026-01-24 15:35:05 +13:00
|
|
|
resolveToolPolicy: resolveWhatsAppGroupToolPolicy,
|
2026-02-23 21:25:20 +00:00
|
|
|
resolveGroupIntroHint: resolveWhatsAppGroupIntroHint,
|
2026-01-11 11:45:25 +00:00
|
|
|
},
|
|
|
|
|
mentions: {
|
2026-02-23 21:25:20 +00:00
|
|
|
stripPatterns: ({ ctx }) => resolveWhatsAppMentionStripPatterns(ctx),
|
2026-01-11 11:45:25 +00:00
|
|
|
},
|
2026-01-13 01:03:23 +00:00
|
|
|
threading: {
|
2026-01-21 20:01:12 +00:00
|
|
|
buildToolContext: ({ context, hasRepliedRef }) => {
|
|
|
|
|
const channelId = context.From?.trim() || context.To?.trim() || undefined;
|
|
|
|
|
return {
|
|
|
|
|
currentChannelId: channelId,
|
|
|
|
|
currentThreadTs: context.ReplyToId,
|
|
|
|
|
hasRepliedRef,
|
|
|
|
|
};
|
|
|
|
|
},
|
2026-01-13 01:03:23 +00:00
|
|
|
},
|
2026-01-11 11:45:25 +00:00
|
|
|
},
|
|
|
|
|
discord: {
|
|
|
|
|
id: "discord",
|
|
|
|
|
capabilities: {
|
|
|
|
|
chatTypes: ["direct", "channel", "thread"],
|
|
|
|
|
polls: true,
|
|
|
|
|
reactions: true,
|
|
|
|
|
media: true,
|
|
|
|
|
nativeCommands: true,
|
|
|
|
|
threads: true,
|
|
|
|
|
},
|
|
|
|
|
outbound: { textChunkLimit: 2000 },
|
2026-02-22 14:05:46 +00:00
|
|
|
streaming: DEFAULT_BLOCK_STREAMING_COALESCE,
|
2026-01-11 11:45:25 +00:00
|
|
|
elevated: {
|
2026-02-15 03:46:11 +01:00
|
|
|
allowFromFallback: ({ cfg }) =>
|
|
|
|
|
cfg.channels?.discord?.allowFrom ?? cfg.channels?.discord?.dm?.allowFrom,
|
2026-01-11 11:45:25 +00:00
|
|
|
},
|
|
|
|
|
config: {
|
2026-02-15 03:46:11 +01:00
|
|
|
resolveAllowFrom: ({ cfg, accountId }) => {
|
2026-03-05 23:07:13 -06:00
|
|
|
const account = inspectDiscordAccount({ cfg, accountId });
|
2026-02-15 03:46:11 +01:00
|
|
|
return (account.config.allowFrom ?? account.config.dm?.allowFrom ?? []).map((entry) =>
|
2026-01-14 14:31:43 +00:00
|
|
|
String(entry),
|
2026-02-15 03:46:11 +01:00
|
|
|
);
|
|
|
|
|
},
|
2026-02-17 08:45:12 -05:00
|
|
|
formatAllowFrom: ({ allowFrom }) => formatDiscordAllowFrom(allowFrom),
|
2026-02-19 23:37:19 -05:00
|
|
|
resolveDefaultTo: ({ cfg, accountId }) =>
|
2026-03-05 23:07:13 -06:00
|
|
|
inspectDiscordAccount({ cfg, accountId }).config.defaultTo?.trim() || undefined,
|
2026-01-11 11:45:25 +00:00
|
|
|
},
|
|
|
|
|
groups: {
|
|
|
|
|
resolveRequireMention: resolveDiscordGroupRequireMention,
|
2026-01-24 15:35:05 +13:00
|
|
|
resolveToolPolicy: resolveDiscordGroupToolPolicy,
|
2026-01-11 11:45:25 +00:00
|
|
|
},
|
|
|
|
|
mentions: {
|
|
|
|
|
stripPatterns: () => ["<@!?\\d+>"],
|
|
|
|
|
},
|
|
|
|
|
threading: {
|
2026-01-14 14:31:43 +00:00
|
|
|
resolveReplyToMode: ({ cfg }) => cfg.channels?.discord?.replyToMode ?? "off",
|
2026-01-13 01:03:23 +00:00
|
|
|
buildToolContext: ({ context, hasRepliedRef }) => ({
|
|
|
|
|
currentChannelId: context.To?.trim() || undefined,
|
|
|
|
|
currentThreadTs: context.ReplyToId,
|
|
|
|
|
hasRepliedRef,
|
|
|
|
|
}),
|
2026-01-11 11:45:25 +00:00
|
|
|
},
|
|
|
|
|
},
|
2026-02-10 15:33:57 -08:00
|
|
|
irc: {
|
|
|
|
|
id: "irc",
|
|
|
|
|
capabilities: {
|
|
|
|
|
chatTypes: ["direct", "group"],
|
|
|
|
|
media: true,
|
|
|
|
|
blockStreaming: true,
|
|
|
|
|
},
|
|
|
|
|
outbound: { textChunkLimit: 350 },
|
|
|
|
|
streaming: {
|
|
|
|
|
blockStreamingCoalesceDefaults: { minChars: 300, idleMs: 1000 },
|
|
|
|
|
},
|
|
|
|
|
config: {
|
|
|
|
|
resolveAllowFrom: ({ cfg, accountId }) => {
|
|
|
|
|
const channel = cfg.channels?.irc;
|
2026-02-18 19:04:57 +00:00
|
|
|
const account = resolveCaseInsensitiveAccount(channel?.accounts, accountId);
|
2026-02-10 15:33:57 -08:00
|
|
|
return (account?.allowFrom ?? channel?.allowFrom ?? []).map((entry) => String(entry));
|
|
|
|
|
},
|
|
|
|
|
formatAllowFrom: ({ allowFrom }) =>
|
2026-02-22 14:05:46 +00:00
|
|
|
formatAllowFromWithReplacements(allowFrom, [/^irc:/i, /^user:/i]),
|
|
|
|
|
resolveDefaultTo: ({ cfg, accountId }) =>
|
|
|
|
|
resolveNamedChannelDefaultTo({
|
|
|
|
|
channels: cfg.channels as CaseInsensitiveDefaultToChannels | undefined,
|
|
|
|
|
channelId: "irc",
|
|
|
|
|
accountId,
|
|
|
|
|
}),
|
2026-02-10 15:33:57 -08:00
|
|
|
},
|
|
|
|
|
groups: {
|
|
|
|
|
resolveRequireMention: ({ cfg, accountId, groupId }) => {
|
|
|
|
|
if (!groupId) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
return resolveChannelGroupRequireMention({
|
|
|
|
|
cfg,
|
|
|
|
|
channel: "irc",
|
|
|
|
|
groupId,
|
|
|
|
|
accountId,
|
|
|
|
|
groupIdCaseInsensitive: true,
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
resolveToolPolicy: ({ cfg, accountId, groupId, senderId, senderName, senderUsername }) => {
|
|
|
|
|
if (!groupId) {
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
// IRC supports per-channel tool policies. Prefer the shared resolver so
|
|
|
|
|
// toolsBySender is honored consistently across surfaces.
|
|
|
|
|
return resolveChannelGroupToolsPolicy({
|
|
|
|
|
cfg,
|
|
|
|
|
channel: "irc",
|
|
|
|
|
groupId,
|
|
|
|
|
accountId,
|
|
|
|
|
groupIdCaseInsensitive: true,
|
|
|
|
|
senderId,
|
|
|
|
|
senderName,
|
|
|
|
|
senderUsername,
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
2026-01-23 16:45:37 -06:00
|
|
|
googlechat: {
|
|
|
|
|
id: "googlechat",
|
|
|
|
|
capabilities: {
|
|
|
|
|
chatTypes: ["direct", "group", "thread"],
|
|
|
|
|
reactions: true,
|
|
|
|
|
media: true,
|
|
|
|
|
threads: true,
|
|
|
|
|
blockStreaming: true,
|
|
|
|
|
},
|
2026-02-22 14:05:46 +00:00
|
|
|
outbound: DEFAULT_OUTBOUND_TEXT_CHUNK_LIMIT_4000,
|
2026-01-23 16:45:37 -06:00
|
|
|
config: {
|
|
|
|
|
resolveAllowFrom: ({ cfg, accountId }) => {
|
|
|
|
|
const channel = cfg.channels?.googlechat as
|
|
|
|
|
| {
|
|
|
|
|
accounts?: Record<string, { dm?: { allowFrom?: Array<string | number> } }>;
|
|
|
|
|
dm?: { allowFrom?: Array<string | number> };
|
|
|
|
|
}
|
|
|
|
|
| undefined;
|
2026-02-18 19:04:57 +00:00
|
|
|
const account = resolveCaseInsensitiveAccount(channel?.accounts, accountId);
|
2026-01-23 16:45:37 -06:00
|
|
|
return (account?.dm?.allowFrom ?? channel?.dm?.allowFrom ?? []).map((entry) =>
|
|
|
|
|
String(entry),
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
formatAllowFrom: ({ allowFrom }) =>
|
2026-02-22 14:05:46 +00:00
|
|
|
formatAllowFromWithReplacements(allowFrom, [
|
|
|
|
|
/^(googlechat|google-chat|gchat):/i,
|
|
|
|
|
/^user:/i,
|
|
|
|
|
/^users\//i,
|
|
|
|
|
]),
|
|
|
|
|
resolveDefaultTo: ({ cfg, accountId }) =>
|
|
|
|
|
resolveNamedChannelDefaultTo({
|
|
|
|
|
channels: cfg.channels as CaseInsensitiveDefaultToChannels | undefined,
|
|
|
|
|
channelId: "googlechat",
|
|
|
|
|
accountId,
|
|
|
|
|
}),
|
2026-01-23 16:45:37 -06:00
|
|
|
},
|
|
|
|
|
groups: {
|
|
|
|
|
resolveRequireMention: resolveGoogleChatGroupRequireMention,
|
|
|
|
|
resolveToolPolicy: resolveGoogleChatGroupToolPolicy,
|
|
|
|
|
},
|
|
|
|
|
threading: {
|
|
|
|
|
resolveReplyToMode: ({ cfg }) => cfg.channels?.googlechat?.replyToMode ?? "off",
|
2026-02-22 07:37:54 +00:00
|
|
|
buildToolContext: ({ context, hasRepliedRef }) =>
|
|
|
|
|
buildThreadToolContextFromMessageThreadOrReply({ context, hasRepliedRef }),
|
2026-01-23 16:45:37 -06:00
|
|
|
},
|
|
|
|
|
},
|
2026-01-11 11:45:25 +00:00
|
|
|
slack: {
|
|
|
|
|
id: "slack",
|
|
|
|
|
capabilities: {
|
|
|
|
|
chatTypes: ["direct", "channel", "thread"],
|
|
|
|
|
reactions: true,
|
|
|
|
|
media: true,
|
|
|
|
|
nativeCommands: true,
|
|
|
|
|
threads: true,
|
|
|
|
|
},
|
2026-02-22 14:05:46 +00:00
|
|
|
outbound: DEFAULT_OUTBOUND_TEXT_CHUNK_LIMIT_4000,
|
|
|
|
|
streaming: DEFAULT_BLOCK_STREAMING_COALESCE,
|
2026-01-11 11:45:25 +00:00
|
|
|
config: {
|
2026-02-15 03:46:11 +01:00
|
|
|
resolveAllowFrom: ({ cfg, accountId }) => {
|
2026-03-05 23:07:13 -06:00
|
|
|
const account = inspectSlackAccount({ cfg, accountId });
|
2026-02-15 03:46:11 +01:00
|
|
|
return (account.config.allowFrom ?? account.dm?.allowFrom ?? []).map((entry) =>
|
|
|
|
|
String(entry),
|
|
|
|
|
);
|
|
|
|
|
},
|
2026-01-11 11:45:25 +00:00
|
|
|
formatAllowFrom: ({ allowFrom }) => formatLower(allowFrom),
|
2026-02-19 23:37:19 -05:00
|
|
|
resolveDefaultTo: ({ cfg, accountId }) =>
|
2026-03-05 23:07:13 -06:00
|
|
|
inspectSlackAccount({ cfg, accountId }).config.defaultTo?.trim() || undefined,
|
2026-01-11 11:45:25 +00:00
|
|
|
},
|
|
|
|
|
groups: {
|
|
|
|
|
resolveRequireMention: resolveSlackGroupRequireMention,
|
2026-01-24 15:35:05 +13:00
|
|
|
resolveToolPolicy: resolveSlackGroupToolPolicy,
|
2026-01-11 11:45:25 +00:00
|
|
|
},
|
2026-02-05 18:29:07 -06:00
|
|
|
mentions: {
|
|
|
|
|
stripPatterns: () => ["<@[^>]+>"],
|
|
|
|
|
},
|
2026-01-11 11:45:25 +00:00
|
|
|
threading: {
|
2026-01-23 07:13:23 +02:00
|
|
|
resolveReplyToMode: ({ cfg, accountId, chatType }) =>
|
2026-03-05 23:07:13 -06:00
|
|
|
resolveSlackReplyToMode(inspectSlackAccount({ cfg, accountId }), chatType),
|
fix(slack): finalize replyToMode off threading behavior (#23799)
* fix: make replyToMode 'off' actually prevent threading in Slack
Three independent bugs caused Slack replies to always create threads
even when replyToMode was set to 'off':
1. Typing indicator created threads via statusThreadTs fallback (#16868)
- resolveSlackThreadTargets fell back to messageTs for statusThreadTs
- 'is typing...' was posted as thread reply, creating a thread
- Fix: remove messageTs fallback, let statusThreadTs be undefined
2. [[reply_to_current]] tags bypassed replyToMode entirely (#16080)
- Slack dock had allowExplicitReplyTagsWhenOff: true
- Reply tags from system prompt always threaded regardless of config
- Fix: set allowExplicitReplyTagsWhenOff to false for Slack
3. Contradictory replyToMode defaults in codebase (#20827)
- monitor/provider.ts defaulted to 'all'
- accounts.ts defaulted to 'off' (matching docs)
- Fix: align provider.ts default to 'off' per documentation
Fixes: openclaw/openclaw#16868, openclaw/openclaw#16080, openclaw/openclaw#20827
* fix(slack): respect replyToMode in DMs even with typing indicator thread
When replyToMode is 'off' in DMs, replies should stay in the main
conversation even when the typing indicator creates a thread context.
Previously, when incomingThreadTs was set (from the typing indicator's
thread), replyToMode was forced to 'all', causing all replies to go
into the thread.
Now, for direct messages, the user's configured replyToMode is always
respected. For channels/groups, the existing behavior is preserved
(stay in thread if already in one).
This fix:
- Keeps the typing indicator working (statusThreadTs fallback preserved)
- Prevents DM replies from being forced into threads
- Maintains channel thread continuity
Fixes #16868
* refactor(slack): eliminate redundant resolveSlackThreadContext call
- Add isThreadReply to resolveSlackThreadTargets return value
- Remove duplicate call in dispatch.ts
- Addresses greptile review feedback with cleaner DRY approach
* docs(slack): add JSDoc to resolveSlackThreadTargets
Document return values including isThreadReply distinction between
genuine user thread replies vs bot status message thread context.
* docs(changelog): record Slack replyToMode off threading fixes
---------
Co-authored-by: James <jamesrp13@gmail.com>
Co-authored-by: theoseo <suhong.seo@gmail.com>
2026-02-22 13:27:50 -05:00
|
|
|
allowExplicitReplyTagsWhenOff: false,
|
2026-01-21 20:01:12 +00:00
|
|
|
buildToolContext: (params) => buildSlackThreadingToolContext(params),
|
2026-01-11 11:45:25 +00:00
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
signal: {
|
|
|
|
|
id: "signal",
|
|
|
|
|
capabilities: {
|
|
|
|
|
chatTypes: ["direct", "group"],
|
|
|
|
|
reactions: true,
|
|
|
|
|
media: true,
|
|
|
|
|
},
|
2026-02-22 14:05:46 +00:00
|
|
|
outbound: DEFAULT_OUTBOUND_TEXT_CHUNK_LIMIT_4000,
|
|
|
|
|
streaming: DEFAULT_BLOCK_STREAMING_COALESCE,
|
2026-01-11 11:45:25 +00:00
|
|
|
config: {
|
|
|
|
|
resolveAllowFrom: ({ cfg, accountId }) =>
|
2026-02-22 07:37:54 +00:00
|
|
|
stringifyAllowFrom(resolveSignalAccount({ cfg, accountId }).config.allowFrom ?? []),
|
2026-01-11 11:45:25 +00:00
|
|
|
formatAllowFrom: ({ allowFrom }) =>
|
2026-02-22 07:37:54 +00:00
|
|
|
trimAllowFromEntries(allowFrom)
|
2026-01-14 14:31:43 +00:00
|
|
|
.map((entry) => (entry === "*" ? "*" : normalizeE164(entry.replace(/^signal:/i, ""))))
|
2026-01-11 11:45:25 +00:00
|
|
|
.filter(Boolean),
|
2026-02-19 23:37:19 -05:00
|
|
|
resolveDefaultTo: ({ cfg, accountId }) =>
|
|
|
|
|
resolveSignalAccount({ cfg, accountId }).config.defaultTo?.trim() || undefined,
|
2026-01-11 11:45:25 +00:00
|
|
|
},
|
2026-01-13 01:03:23 +00:00
|
|
|
threading: {
|
2026-02-15 18:30:34 +00:00
|
|
|
buildToolContext: ({ context, hasRepliedRef }) =>
|
2026-02-17 14:17:22 -08:00
|
|
|
buildSignalThreadToolContext({ context, hasRepliedRef }),
|
2026-01-13 01:03:23 +00:00
|
|
|
},
|
2026-01-11 11:45:25 +00:00
|
|
|
},
|
|
|
|
|
imessage: {
|
|
|
|
|
id: "imessage",
|
|
|
|
|
capabilities: {
|
|
|
|
|
chatTypes: ["direct", "group"],
|
|
|
|
|
reactions: true,
|
|
|
|
|
media: true,
|
|
|
|
|
},
|
2026-02-22 14:05:46 +00:00
|
|
|
outbound: DEFAULT_OUTBOUND_TEXT_CHUNK_LIMIT_4000,
|
2026-01-11 11:45:25 +00:00
|
|
|
config: {
|
2026-03-02 08:53:11 +00:00
|
|
|
resolveAllowFrom: ({ cfg, accountId }) => resolveIMessageConfigAllowFrom({ cfg, accountId }),
|
|
|
|
|
formatAllowFrom: ({ allowFrom }) => formatTrimmedAllowFromEntries(allowFrom),
|
|
|
|
|
resolveDefaultTo: ({ cfg, accountId }) => resolveIMessageConfigDefaultTo({ cfg, accountId }),
|
2026-01-11 11:45:25 +00:00
|
|
|
},
|
|
|
|
|
groups: {
|
|
|
|
|
resolveRequireMention: resolveIMessageGroupRequireMention,
|
2026-01-24 15:35:05 +13:00
|
|
|
resolveToolPolicy: resolveIMessageGroupToolPolicy,
|
2026-01-11 11:45:25 +00:00
|
|
|
},
|
2026-01-13 01:03:23 +00:00
|
|
|
threading: {
|
2026-02-15 18:30:34 +00:00
|
|
|
buildToolContext: ({ context, hasRepliedRef }) =>
|
2026-02-17 14:17:22 -08:00
|
|
|
buildIMessageThreadToolContext({ context, hasRepliedRef }),
|
2026-01-13 01:03:23 +00:00
|
|
|
},
|
2026-01-11 11:45:25 +00:00
|
|
|
},
|
Fix/Complete LINE `requireMention` gating behavior (#35847)
* fix(line): enforce requireMention gating in group message handler
* fix(line): scope canDetectMention to text messages, pass hasAnyMention
* fix(line): fix TS errors in mentionees type and test casts
* feat(line): register LINE in DOCKS and CHAT_CHANNEL_ORDER
- Add "line" to CHAT_CHANNEL_ORDER and CHAT_CHANNEL_META in registry.ts
- Export resolveLineGroupRequireMention and resolveLineGroupToolPolicy
in group-mentions.ts using the generic resolveChannelGroupRequireMention
and resolveChannelGroupToolsPolicy helpers (same pattern as iMessage)
- Add "line" entry to DOCKS in dock.ts so resolveGroupRequireMention
in the reply stage can correctly read LINE group config
Fixes the third layer of the requireMention bug: previously
getChannelDock("line") returned undefined, causing the reply-stage
resolveGroupRequireMention to fall back to true unconditionally.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(line): pending history, requireMention default, mentionPatterns fallback
- Default requireMention to true (consistent with other channels)
- Add mentionPatterns regex fallback alongside native isSelf/@all detection
- Record unmentioned group messages via recordPendingHistoryEntryIfEnabled
- Inject pending history context in buildLineMessageContext when bot is mentioned
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(line): update tests for requireMention default and pending history
- Add requireMention: false to 6 group tests unrelated to mention gating
(allowlist, replay dedup, inflight dedup, error retry) to preserve
their original intent after the default changed from false to true
- Add test: skips group messages by default when requireMention not configured
- Add test: records unmentioned group messages as pending history
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(line): use undefined instead of empty string as historyKey sentinel
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(line): deliver pending history via InboundHistory, not Body mutation
- Remove post-hoc ctxPayload.Body injection (BodyForAgent takes priority
in the prompt pipeline, so Body was never reached)
- Pass InboundHistory array to finalizeInboundContext instead, matching
the Telegram pattern rendered by buildInboundUserContextPrefix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(line): pass agentId to buildMentionRegexes for per-agent mentionPatterns
- Resolve route before mention gating to obtain agentId
- Pass agentId to buildMentionRegexes, matching Telegram behavior
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(line): clear pending history after handled group turn
- Call clearHistoryEntriesIfEnabled after processMessage for group messages
- Prevents stale skipped messages from replaying on subsequent mentions
- Matches Discord, Signal, Slack, iMessage behavior
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* style(line): fix import order and merge orphaned JSDoc in bot-handlers
- Move resolveAgentRoute import from ./local group to ../routing group
- Merge duplicate JSDoc blocks above getLineMentionees into one
Addresses Greptile review comments r2888826724 and r2888826840 on PR #35847.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(line): read historyLimit from config and guard clear with has()
- bot.ts: resolve historyLimit from cfg.messages.groupChat.historyLimit
with fallback to DEFAULT_GROUP_HISTORY_LIMIT, so setting historyLimit: 0
actually disables pending history accumulation
- bot-handlers.ts: add groupHistories.has(historyKey) guard before
clearHistoryEntriesIfEnabled to prevent writing empty buckets for
groups that have never accumulated pending history (memory leak)
Addresses Codex review comments r2888829146 and r2888829152 on PR #35847.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* style(line): apply oxfmt formatting to bot-handlers and bot
Auto-formatted by oxfmt to fix CI format:check failure on PR #35847.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(line): add shouldLogVerbose to globals mock in bot-handlers test
resolveAgentRoute calls shouldLogVerbose() from globals.js; the mock
was missing this export, causing 13 test failures.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Address review findings for #35847
---------
Co-authored-by: Kaiyi <me@kaiyi.cool>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Yi-Cheng Wang <yicheng.wang@heph-ai.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
2026-03-08 04:06:07 +08:00
|
|
|
line: {
|
|
|
|
|
id: "line",
|
|
|
|
|
capabilities: {
|
|
|
|
|
chatTypes: ["direct", "group"],
|
|
|
|
|
media: true,
|
|
|
|
|
},
|
|
|
|
|
outbound: { textChunkLimit: 5000 },
|
|
|
|
|
groups: {
|
|
|
|
|
resolveRequireMention: resolveLineGroupRequireMention,
|
|
|
|
|
resolveToolPolicy: resolveLineGroupToolPolicy,
|
|
|
|
|
},
|
|
|
|
|
},
|
2026-01-11 11:45:25 +00:00
|
|
|
};
|
|
|
|
|
|
2026-01-15 02:42:41 +00:00
|
|
|
function buildDockFromPlugin(plugin: ChannelPlugin): ChannelDock {
|
|
|
|
|
return {
|
|
|
|
|
id: plugin.id,
|
|
|
|
|
capabilities: plugin.capabilities,
|
|
|
|
|
commands: plugin.commands,
|
|
|
|
|
outbound: plugin.outbound?.textChunkLimit
|
|
|
|
|
? { textChunkLimit: plugin.outbound.textChunkLimit }
|
|
|
|
|
: undefined,
|
|
|
|
|
streaming: plugin.streaming
|
|
|
|
|
? { blockStreamingCoalesceDefaults: plugin.streaming.blockStreamingCoalesceDefaults }
|
|
|
|
|
: undefined,
|
|
|
|
|
elevated: plugin.elevated,
|
|
|
|
|
config: plugin.config
|
|
|
|
|
? {
|
|
|
|
|
resolveAllowFrom: plugin.config.resolveAllowFrom,
|
|
|
|
|
formatAllowFrom: plugin.config.formatAllowFrom,
|
2026-02-19 23:37:19 -05:00
|
|
|
resolveDefaultTo: plugin.config.resolveDefaultTo,
|
2026-01-15 02:42:41 +00:00
|
|
|
}
|
|
|
|
|
: undefined,
|
|
|
|
|
groups: plugin.groups,
|
|
|
|
|
mentions: plugin.mentions,
|
|
|
|
|
threading: plugin.threading,
|
2026-01-22 03:27:26 +00:00
|
|
|
agentPrompt: plugin.agentPrompt,
|
2026-01-15 02:42:41 +00:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function listPluginDockEntries(): Array<{ id: ChannelId; dock: ChannelDock; order?: number }> {
|
2026-01-18 11:00:19 +00:00
|
|
|
const registry = requireActivePluginRegistry();
|
2026-01-15 02:42:41 +00:00
|
|
|
const entries: Array<{ id: ChannelId; dock: ChannelDock; order?: number }> = [];
|
|
|
|
|
const seen = new Set<string>();
|
|
|
|
|
for (const entry of registry.channels) {
|
|
|
|
|
const plugin = entry.plugin;
|
|
|
|
|
const id = String(plugin.id).trim();
|
2026-01-31 16:19:20 +09:00
|
|
|
if (!id || seen.has(id)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2026-01-15 02:42:41 +00:00
|
|
|
seen.add(id);
|
2026-01-31 16:19:20 +09:00
|
|
|
if (CHAT_CHANNEL_ORDER.includes(plugin.id as ChatChannelId)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2026-01-15 02:42:41 +00:00
|
|
|
const dock = entry.dock ?? buildDockFromPlugin(plugin);
|
|
|
|
|
entries.push({ id: plugin.id, dock, order: plugin.meta.order });
|
|
|
|
|
}
|
|
|
|
|
return entries;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-13 06:16:43 +00:00
|
|
|
export function listChannelDocks(): ChannelDock[] {
|
2026-01-15 02:42:41 +00:00
|
|
|
const baseEntries = CHAT_CHANNEL_ORDER.map((id) => ({
|
|
|
|
|
id,
|
|
|
|
|
dock: DOCKS[id],
|
|
|
|
|
order: getChatChannelMeta(id).order,
|
|
|
|
|
}));
|
|
|
|
|
const pluginEntries = listPluginDockEntries();
|
|
|
|
|
const combined = [...baseEntries, ...pluginEntries];
|
|
|
|
|
combined.sort((a, b) => {
|
|
|
|
|
const indexA = CHAT_CHANNEL_ORDER.indexOf(a.id as ChatChannelId);
|
|
|
|
|
const indexB = CHAT_CHANNEL_ORDER.indexOf(b.id as ChatChannelId);
|
|
|
|
|
const orderA = a.order ?? (indexA === -1 ? 999 : indexA);
|
|
|
|
|
const orderB = b.order ?? (indexB === -1 ? 999 : indexB);
|
2026-01-31 16:19:20 +09:00
|
|
|
if (orderA !== orderB) {
|
|
|
|
|
return orderA - orderB;
|
|
|
|
|
}
|
2026-01-15 02:42:41 +00:00
|
|
|
return String(a.id).localeCompare(String(b.id));
|
|
|
|
|
});
|
|
|
|
|
return combined.map((entry) => entry.dock);
|
2026-01-11 11:45:25 +00:00
|
|
|
}
|
|
|
|
|
|
2026-01-13 06:16:43 +00:00
|
|
|
export function getChannelDock(id: ChannelId): ChannelDock | undefined {
|
2026-01-15 02:42:41 +00:00
|
|
|
const core = DOCKS[id as ChatChannelId];
|
2026-01-31 16:19:20 +09:00
|
|
|
if (core) {
|
|
|
|
|
return core;
|
|
|
|
|
}
|
2026-01-18 11:00:19 +00:00
|
|
|
const registry = requireActivePluginRegistry();
|
|
|
|
|
const pluginEntry = registry.channels.find((entry) => entry.plugin.id === id);
|
2026-01-31 16:19:20 +09:00
|
|
|
if (!pluginEntry) {
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
2026-01-15 02:42:41 +00:00
|
|
|
return pluginEntry.dock ?? buildDockFromPlugin(pluginEntry.plugin);
|
2026-01-11 11:45:25 +00:00
|
|
|
}
|