mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(clickclack): isolate bot loop budgets by thread
This commit is contained in:
@@ -469,7 +469,9 @@ Accepted bot messages also pass through OpenClaw's shared bot-pair loop guard.
|
||||
Use `botLoopProtection` on the account or `channels.defaults.botLoopProtection`
|
||||
to tune its window, budget, cooldown, or enabled state. Group-level `allowBots`
|
||||
and `botLoopProtection` values follow the same exact-channel, wildcard, then
|
||||
account-level precedence as the other group policies.
|
||||
account-level precedence as the other group policies. Top-level channel
|
||||
messages share a channel budget, while replies in different ClickClack threads
|
||||
use independent thread-root budgets.
|
||||
|
||||
Older ClickClack responses may omit `author.kind`. Those messages intentionally
|
||||
remain on the legacy `allowFrom` path: `allowFrom: ["*"]` can admit them, and
|
||||
|
||||
@@ -53,6 +53,18 @@ type ClickClackPreparedInboundRoute = {
|
||||
revoked: boolean;
|
||||
};
|
||||
|
||||
function resolveClickClackBotLoopConversationId(params: {
|
||||
message: ClickClackMessage;
|
||||
isDirect: boolean;
|
||||
}): string {
|
||||
if (params.message.parent_message_id && params.message.thread_root_id) {
|
||||
return params.message.thread_root_id;
|
||||
}
|
||||
return params.isDirect
|
||||
? (params.message.direct_conversation_id ?? params.message.author_id)
|
||||
: (params.message.channel_id ?? params.message.thread_root_id ?? params.message.author_id);
|
||||
}
|
||||
|
||||
function resolveAccountAgentRoute(params: {
|
||||
cfg: OpenClawConfig;
|
||||
account: ResolvedClickClackAccount;
|
||||
@@ -235,11 +247,10 @@ export async function resolveClickClackInboundAccess(params: {
|
||||
// The workspace is the shared boundary; account IDs would let the
|
||||
// same conversation evade the budget by alternating receivers.
|
||||
scopeId: params.account.workspace,
|
||||
conversationId: preparedRoute.isDirect
|
||||
? (params.message.direct_conversation_id ?? params.message.author_id)
|
||||
: (params.message.channel_id ??
|
||||
params.message.thread_root_id ??
|
||||
params.message.author_id),
|
||||
conversationId: resolveClickClackBotLoopConversationId({
|
||||
message: params.message,
|
||||
isDirect: preparedRoute.isDirect,
|
||||
}),
|
||||
senderId: params.message.author_id,
|
||||
receiverId: params.account.botUserId,
|
||||
eventId: params.message.id,
|
||||
|
||||
@@ -50,9 +50,7 @@ export function resolveClickClackBotPolicy(params: {
|
||||
// account-wide override.
|
||||
const groups = channelKey ? account.groups : undefined;
|
||||
const wildcard = groups?.["*"];
|
||||
const exact = channelKey
|
||||
? Object.entries(groups ?? {}).find(([key]) => key.trim() === channelKey)?.[1]
|
||||
: undefined;
|
||||
const exact = channelKey ? groups?.[channelKey] : undefined;
|
||||
return {
|
||||
allowBots: exact?.allowBots ?? wildcard?.allowBots ?? account.allowBots ?? false,
|
||||
botLoopProtection: mergePairLoopGuardConfig(
|
||||
|
||||
@@ -286,11 +286,45 @@ describe("ClickClack inbound mention gating", () => {
|
||||
conversationId: "chn_1",
|
||||
senderId: "usr_sender",
|
||||
receiverId: "usr_receiver",
|
||||
eventId: "msg_1",
|
||||
defaultsConfig: { maxEventsPerWindow: 7 },
|
||||
defaultEnabled: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("isolates bot loop budgets by ClickClack thread root", async () => {
|
||||
const runtime = createRuntime();
|
||||
setClickClackRuntime(runtime);
|
||||
const account = createAgentAccount({ allowFrom: ["usr_sender"], allowBots: true });
|
||||
const author = createAuthor({ id: "usr_sender", kind: "bot", handle: "sender" });
|
||||
|
||||
const threadA = await resolveClickClackInboundAccess({
|
||||
account,
|
||||
config: {} satisfies CoreConfig,
|
||||
message: createMessage({
|
||||
id: "msg_thread_a_reply",
|
||||
author_id: "usr_sender",
|
||||
parent_message_id: "msg_thread_a",
|
||||
thread_root_id: "msg_thread_a",
|
||||
author,
|
||||
}),
|
||||
});
|
||||
const threadB = await resolveClickClackInboundAccess({
|
||||
account,
|
||||
config: {} satisfies CoreConfig,
|
||||
message: createMessage({
|
||||
id: "msg_thread_b_reply",
|
||||
author_id: "usr_sender",
|
||||
parent_message_id: "msg_thread_b",
|
||||
thread_root_id: "msg_thread_b",
|
||||
author,
|
||||
}),
|
||||
});
|
||||
|
||||
expect(threadA.botLoopProtection?.conversationId).toBe("msg_thread_a");
|
||||
expect(threadB.botLoopProtection?.conversationId).toBe("msg_thread_b");
|
||||
});
|
||||
|
||||
it("does not let bot opt-in bypass the wildcard human allowFrom default", async () => {
|
||||
const runtime = createRuntime();
|
||||
setClickClackRuntime(runtime);
|
||||
|
||||
Reference in New Issue
Block a user