Skip to content

feat: add group chat message wait time#66

Closed
Juvwxyz wants to merge 3 commits intoChatLunaLab:mainfrom
Juvwxyz:main
Closed

feat: add group chat message wait time#66
Juvwxyz wants to merge 3 commits intoChatLunaLab:mainfrom
Juvwxyz:main

Conversation

@Juvwxyz
Copy link
Copy Markdown
Contributor

@Juvwxyz Juvwxyz commented Apr 2, 2026

Summary

引入群聊消息等待时间能力,减少用户连续发送多条消息时被过早打断的问题。

Changes

  • 为全局群聊配置和单群配置新增 messageWaitTime,默认值为 0
  • 在群聊场景下,当 messageInterval = 0 时,不再收到消息就立刻触发,而是等待同一用户在指定时间内不再继续发送消息后再回复
  • 在群聊中被 @、引用或昵称触发时,也支持按 messageWaitTime 延迟触发,避免用户消息尚未发完就开始回复
  • 在调度器中新增待触发状态管理,统一处理延迟触发、消费已满足条件的触发项,并在触发后清理对应状态
  • 补充相关类型与默认状态初始化,确保新触发流程可持久化运行

Impact

  • 默认配置下行为基本不变
  • 配置 messageWaitTime > 0 后,群聊回复会更贴近“等用户说完再答”的交互预期
  • 对高频群聊、分段发送消息、@ 机器人后连续补充上下文的场景更友好

Summary by CodeRabbit

  • New Features

    • Added messageWaitTime (0–300s) to delay aggregation in group chats so replies wait until a user is idle.
  • Improvements

    • Improved streaming behavior to reduce failed or duplicate replies and make responses more consistent.
    • Enhanced trigger scheduling for group chats: mentions and rapid messages can now be deferred and batched until readiness, reducing premature replies.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 2, 2026

📝 Walkthrough

Walkthrough

Adds a configurable messageWaitTime and deferred trigger handling for group chats, tracks pending direct/user triggers, updates scheduler to consume them, adjusts streaming reply logic (removes retry loop and refines empty-reply detection), and initializes pending-trigger fields in default group info.

Changes

Cohort / File(s) Summary
Configuration
src/config.ts
Added messageWaitTime numeric field (default 0, min 0, max 300) to global and per-guild group config schemas.
Type Definitions
src/types.ts
Extended GuildConfig with messageWaitTime; added pendingDirectTrigger? and pendingUserTriggers? to GroupInfo; simplified PrivateConfig inheritance.
Service Layer
src/service/trigger.ts
createDefaultGroupInfo now initializes pendingDirectTrigger and pendingUserTriggers in returned GroupInfo.
Trigger & Scheduler Logic
src/plugins/filter.ts
Deferred triggering for group chats when messageInterval === 0 and/or messageWaitTime > 0; added storage and evaluators for pendingDirectTrigger and pendingUserTriggers; updated hasPendingSchedulerWork and processSchedulerTickForGuild to prioritize pending triggers and clean consumed entries.
Response Streaming & Reply Logic
src/plugins/chat.ts
Removed retry loop from streamModelResponse; simplified streaming vs non-stream handling; changed empty vs non-empty reply detection to use canSend (elements length > 0); removed unconditional early return and removed mute call from the !sentAny path.

Sequence Diagram

sequenceDiagram
    participant User as User (Group Chat)
    participant Filter as Filter Plugin
    participant Scheduler as Scheduler
    participant Chat as Chat Plugin
    participant Model as Model/Stream

    User->>Filter: Send message (mention or normal)
    Filter->>Filter: Determine immediateTriggerReason
    alt direct mention and messageWaitTime > 0
        Filter->>Filter: Store `pendingDirectTrigger` (userId, reason), increment messageCount
        Filter-->>User: return (deferred)
    else messageInterval === 0
        Filter->>Filter: Store in `pendingUserTriggers` with lastMessageAt, increment messageCount
        Filter-->>User: return (deferred)
    end

    Note over Scheduler,Filter: Scheduler tick runs
    Scheduler->>Filter: hasPendingSchedulerWork?
    Filter-->>Scheduler: true (pending triggers exist)
    Scheduler->>Filter: find ready pending triggers (compare lastMessageAt vs messageWaitTime)
    Filter-->>Scheduler: ready triggerReason + readyUserIds
    Scheduler->>Chat: Trigger collection for ready users
    Chat->>Model: stream(...) or invoke(...)
    Model-->>Chat: parsed response chunks
    Chat->>Chat: compute canSend (elements.length > 0)
    alt canSend or raw message
        Chat-->>User: send reply
    else
        Chat-->>User: (no send)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • dingyi222666

Poem

🐰 In the meadow we pause and wait,
Messages pile by the gate—
A patient hop, a silent chew,
We tally the hops before we cue,
Then answer together, calm and great.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add group chat message wait time' clearly and specifically summarizes the main change introduced in the PR: a new messageWaitTime feature for group chats.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a message waiting mechanism to aggregate consecutive user messages before triggering a response, refactoring the trigger logic to use a scheduler. Feedback highlights that removing retry logic and safety muting in the chat plugin reduces system robustness and protection against infinite loops. Additionally, the implementation may introduce latency when wait times are zero and skips activity score updates due to early returns in the filter plugin. It is also recommended to clear all pending trigger states upon successful response to prevent duplicate replies.

Comment thread src/plugins/chat.ts
for (let retryCount = 0; retryCount < 2; retryCount++) {
if (signal?.aborted) return
let emittedAny = false
if (signal?.aborted) return
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

重构后的 streamModelResponse 移除了原有的重试逻辑(retryCount 循环和 sleep)。这会导致在模型 API 出现瞬时网络抖动或限流时,插件直接报错而不再尝试恢复,降低了系统的健壮性。建议保留重试机制,以应对不稳定的网络环境。

Comment thread src/plugins/filter.ts
Comment on lines +43 to 44
info.pendingDirectTrigger = undefined
info.pendingNextReplies = []
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

markTriggered 函数中,应当同时清理 pendingUserTriggers。如果机器人因为其他原因(如直接触发或 next_reply)完成了回复,之前的待触发状态也应视作已处理并清空,否则会导致调度器在下一轮 tick 时因为旧的状态而重复触发回复。

    info.pendingDirectTrigger = undefined
    info.pendingUserTriggers = {}
    info.pendingNextReplies = []

Comment thread src/plugins/chat.ts
return
}
service.mute(session, copyOfConfig.muteTime * 1000)
return
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

此处移除了当 !sentAny 时的 service.mute 调用。这个静音逻辑通常用于在模型持续输出无效内容或发生错误时进行保护,防止机器人陷入无意义的频繁触发循环。建议保留此安全机制,或者提供其他的防抖/限流方案。

Comment thread src/plugins/filter.ts
Comment on lines +324 to +326
// For group chats with messageInterval === 0, don't trigger immediately
// Instead, handle via pendingUserTriggers in the scheduler
return undefined
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

messageWaitTime 配置为 0 时,建议维持原有的立即触发逻辑。目前的改动会导致所有 messageInterval === 0 的群聊消息都必须等待调度器(Scheduler)的下一轮轮询才能触发,这会引入不必要的延迟(取决于 SCHEDULER_TICK 的频率),导致默认配置下的响应速度变慢。建议仅在 messageWaitTime > 0 时才返回 undefined

Comment thread src/plugins/filter.ts
Comment on lines +915 to +930
if (!session.isDirect && copyOfConfig.messageInterval === 0) {
const reason = isDirectTrigger
? isAppel
? 'Mention or quote trigger'
: 'Nickname trigger'
: 'Message interval reached (0 mode)'
info.pendingUserTriggers = info.pendingUserTriggers ?? {}
info.pendingUserTriggers[message.id] = {
reason,
lastMessageAt: now,
isDirectTrigger
}
info.messageCount++
store.set(key, info)
return
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

在此处提前返回会导致后续的活跃度分数(Activity Score)更新逻辑被跳过。这意味着当启用消息等待功能时,活跃度统计将无法获取到这些消息的数据,导致统计失效。建议在 return 之前执行活跃度分数的计算和更新,以确保统计数据的完整性。

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/config.ts (1)

399-413: ⚠️ Potential issue | 🟡 Minor

Update the messageInterval = 0 help text to match the new runtime behavior.

These descriptions still say zero-interval mode triggers on every message immediately, but the new group path waits for messageWaitTime before replying. The config UI will otherwise describe the old behavior.

Also applies to: 470-484

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/config.ts` around lines 399 - 413, Update the help text for the
messageInterval Schema entry so its description reflects the new runtime
behavior: when messageInterval is 0, replies are not sent immediately on every
message in group chats but instead wait for messageWaitTime seconds before
replying; reference the messageInterval and messageWaitTime symbols and update
both occurrences (the one shown and the other at the later block) so the UI text
explains the zero-interval mode waits messageWaitTime seconds in group/@-trigger
scenarios rather than triggering instantly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/plugins/chat.ts`:
- Around line 619-665: The catch block currently swallows model failures; change
it to perform a single retry after a 3s delay and then propagate the error if
retry also fails: on any exception (and when !signal?.aborted) await a 3s sleep,
then retry the same logic that sends chunks via streamAgentResponseContents (or
calls model.invoke with createStreamConfig and processes via
parseResponseContent/getMessageContent) exactly once; if the retry fails,
rethrow the error (or let it bubble) instead of silently returning, and ensure
existing logging via logger.error remains on final failure. Use the same
parameters (chain, session, model, presetName, systemMessage, historyMessages,
lastMessage, signal) for the retry so behavior matches the first attempt.

In `@src/plugins/filter.ts`:
- Around line 914-930: The branch that enqueues group messages into
info.pendingUserTriggers when copyOfConfig.messageInterval === 0 must honor the
enableFixedIntervalTrigger toggle; update the condition around session.isDirect
&& copyOfConfig.messageInterval === 0 to also require
copyOfConfig.enableFixedIntervalTrigger (or check enableFixedIntervalTrigger
before creating pendingUserTriggers) so ordinary group messages are not queued
when fixed-interval triggering is disabled; adjust the logic around
info.pendingUserTriggers, message.id, info.messageCount and store.set(key, info)
to only run when enableFixedIntervalTrigger is true.
- Around line 932-947: The current check for shouldWaitDirectTrigger relies on
immediateTriggerReason matching the direct-trigger string, but
resolveImmediateTriggerReason() may return the interval reason when
info.messageCount >= messageInterval, causing direct mentions to skip the wait;
change the logic so the direct-wait path is triggered whenever isDirectTrigger
is true (and other existing guards) rather than depending on
immediateTriggerReason equality. Concretely, update the shouldWaitDirectTrigger
condition to remove the immediateTriggerReason === (...) check (keep
!session.isDirect, isDirectTrigger, copyOfConfig.messageWaitTime > 0), and when
setting info.pendingDirectTrigger set reason to the explicit direct reason (use
the same string used before: 'Mention or quote trigger' or 'Nickname trigger'
based on isAppel) instead of immediateTriggerReason; this ensures direct
mentions always enter the wait path even if the interval reason was computed
first by resolveImmediateTriggerReason().

---

Outside diff comments:
In `@src/config.ts`:
- Around line 399-413: Update the help text for the messageInterval Schema entry
so its description reflects the new runtime behavior: when messageInterval is 0,
replies are not sent immediately on every message in group chats but instead
wait for messageWaitTime seconds before replying; reference the messageInterval
and messageWaitTime symbols and update both occurrences (the one shown and the
other at the later block) so the UI text explains the zero-interval mode waits
messageWaitTime seconds in group/@-trigger scenarios rather than triggering
instantly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 77fbe7ee-3fc3-4cd0-b164-cccf35b54d0c

📥 Commits

Reviewing files that changed from the base of the PR and between 2cfa4fb and c7a7aab.

📒 Files selected for processing (5)
  • src/config.ts
  • src/plugins/chat.ts
  • src/plugins/filter.ts
  • src/service/trigger.ts
  • src/types.ts

Comment thread src/plugins/chat.ts
Comment on lines +619 to 665
try {
const lastMessage =
completionMessages[completionMessages.length - 1]
const historyMessages = completionMessages.slice(0, -1)

const systemMessage =
chain != null ? historyMessages.shift() : undefined
const systemMessage =
chain != null ? historyMessages.shift() : undefined

if (chain) {
for await (const responseChunk of streamAgentResponseContents(
chain,
if (chain) {
for await (const responseChunk of streamAgentResponseContents(
chain,
session,
model,
presetName,
systemMessage,
historyMessages,
lastMessage,
signal
)) {
yield await parseResponseContent(
ctx,
session,
model,
presetName,
systemMessage,
historyMessages,
lastMessage,
signal
)) {
emittedAny = true

yield await parseResponseContent(
ctx,
session,
config,
responseChunk
)
}

return
}

const responseMessage = await model.invoke(
completionMessages,
createStreamConfig(session, model, presetName, signal)
)
const responseContent = getMessageContent(responseMessage.content)
if (responseContent.trim().length < 1) {
return
config,
responseChunk
)
}

logger.debug(`model response:\n${responseContent}`)
emittedAny = true

yield await parseResponseContent(ctx, session, config, {
responseMessage,
responseContent,
isIntermediate: false
})
return
} catch (e) {
if (signal?.aborted) return
logger.error('model requests failed', e)
if (emittedAny || retryCount === 1) return
await sleep(3000)
}

const responseMessage = await model.invoke(
completionMessages,
createStreamConfig(session, model, presetName, signal)
)
const responseContent = getMessageContent(responseMessage.content)

logger.debug(`model response:\n${responseContent}`)

yield await parseResponseContent(ctx, session, config, {
responseMessage,
responseContent,
isIntermediate: false
})
} catch (e) {
if (signal?.aborted) return
logger.error('model requests failed', e)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Restore the single retry here instead of turning model failures into a silent no-op.

This catch now logs and ends the generator. A transient LLM/network failure yields no chunks, no retry, and no propagated error, so the caller just returns without replying.

Based on learnings: Applies to src/**/*.ts : For streaming retry: catch, sleep 3s, retry once, then propagate

🧰 Tools
🪛 ESLint

[error] 620-621: Delete ⏎···········

(prettier/prettier)

🪛 GitHub Check: CodeFactor

[warning] 620-620: src/plugins/chat.ts#L620
Delete ⏎··········· (prettier/prettier)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/plugins/chat.ts` around lines 619 - 665, The catch block currently
swallows model failures; change it to perform a single retry after a 3s delay
and then propagate the error if retry also fails: on any exception (and when
!signal?.aborted) await a 3s sleep, then retry the same logic that sends chunks
via streamAgentResponseContents (or calls model.invoke with createStreamConfig
and processes via parseResponseContent/getMessageContent) exactly once; if the
retry fails, rethrow the error (or let it bubble) instead of silently returning,
and ensure existing logging via logger.error remains on final failure. Use the
same parameters (chain, session, model, presetName, systemMessage,
historyMessages, lastMessage, signal) for the retry so behavior matches the
first attempt.

Comment thread src/plugins/filter.ts
Comment on lines +914 to +930
// For group chats with messageInterval === 0, handle via pendingUserTriggers
if (!session.isDirect && copyOfConfig.messageInterval === 0) {
const reason = isDirectTrigger
? isAppel
? 'Mention or quote trigger'
: 'Nickname trigger'
: 'Message interval reached (0 mode)'
info.pendingUserTriggers = info.pendingUserTriggers ?? {}
info.pendingUserTriggers[message.id] = {
reason,
lastMessageAt: now,
isDirectTrigger
}
info.messageCount++
store.set(key, info)
return
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Honor enableFixedIntervalTrigger before enqueuing zero-interval group triggers.

This branch runs for every group message when messageInterval === 0, even when fixed-interval triggering is disabled. In that configuration, ordinary group messages still get queued into pendingUserTriggers and later fired by the scheduler, so the toggle no longer disables zero-interval auto replies.

Minimal fix
-        if (!session.isDirect && copyOfConfig.messageInterval === 0) {
+        if (
+            !session.isDirect &&
+            copyOfConfig.enableFixedIntervalTrigger !== false &&
+            copyOfConfig.messageInterval === 0
+        ) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// For group chats with messageInterval === 0, handle via pendingUserTriggers
if (!session.isDirect && copyOfConfig.messageInterval === 0) {
const reason = isDirectTrigger
? isAppel
? 'Mention or quote trigger'
: 'Nickname trigger'
: 'Message interval reached (0 mode)'
info.pendingUserTriggers = info.pendingUserTriggers ?? {}
info.pendingUserTriggers[message.id] = {
reason,
lastMessageAt: now,
isDirectTrigger
}
info.messageCount++
store.set(key, info)
return
}
// For group chats with messageInterval === 0, handle via pendingUserTriggers
if (
!session.isDirect &&
copyOfConfig.enableFixedIntervalTrigger !== false &&
copyOfConfig.messageInterval === 0
) {
const reason = isDirectTrigger
? isAppel
? 'Mention or quote trigger'
: 'Nickname trigger'
: 'Message interval reached (0 mode)'
info.pendingUserTriggers = info.pendingUserTriggers ?? {}
info.pendingUserTriggers[message.id] = {
reason,
lastMessageAt: now,
isDirectTrigger
}
info.messageCount++
store.set(key, info)
return
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/plugins/filter.ts` around lines 914 - 930, The branch that enqueues group
messages into info.pendingUserTriggers when copyOfConfig.messageInterval === 0
must honor the enableFixedIntervalTrigger toggle; update the condition around
session.isDirect && copyOfConfig.messageInterval === 0 to also require
copyOfConfig.enableFixedIntervalTrigger (or check enableFixedIntervalTrigger
before creating pendingUserTriggers) so ordinary group messages are not queued
when fixed-interval triggering is disabled; adjust the logic around
info.pendingUserTriggers, message.id, info.messageCount and store.set(key, info)
to only run when enableFixedIntervalTrigger is true.

Comment thread src/plugins/filter.ts
Comment on lines +932 to +947
const shouldWaitDirectTrigger =
!session.isDirect &&
isDirectTrigger &&
copyOfConfig.messageWaitTime > 0 &&
immediateTriggerReason ===
(isAppel ? 'Mention or quote trigger' : 'Nickname trigger')

if (shouldWaitDirectTrigger) {
info.pendingDirectTrigger = {
userId: message.id,
reason: immediateTriggerReason
}
info.messageCount++
store.set(key, info)
return
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Direct mentions stop waiting once the interval counter is already hot.

shouldWaitDirectTrigger only fires when immediateTriggerReason is the direct-trigger string. If info.messageCount >= messageInterval, resolveImmediateTriggerReason() returns the interval reason first, so an @/nickname message in a busy group skips the new wait-time path and replies immediately.

Minimal fix
         const shouldWaitDirectTrigger =
             !session.isDirect &&
             isDirectTrigger &&
             copyOfConfig.messageWaitTime > 0 &&
-            immediateTriggerReason ===
-                (isAppel ? 'Mention or quote trigger' : 'Nickname trigger')
+            copyOfConfig.messageInterval !== 0
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const shouldWaitDirectTrigger =
!session.isDirect &&
isDirectTrigger &&
copyOfConfig.messageWaitTime > 0 &&
immediateTriggerReason ===
(isAppel ? 'Mention or quote trigger' : 'Nickname trigger')
if (shouldWaitDirectTrigger) {
info.pendingDirectTrigger = {
userId: message.id,
reason: immediateTriggerReason
}
info.messageCount++
store.set(key, info)
return
}
const shouldWaitDirectTrigger =
!session.isDirect &&
isDirectTrigger &&
copyOfConfig.messageWaitTime > 0 &&
copyOfConfig.messageInterval !== 0
if (shouldWaitDirectTrigger) {
info.pendingDirectTrigger = {
userId: message.id,
reason: immediateTriggerReason
}
info.messageCount++
store.set(key, info)
return
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/plugins/filter.ts` around lines 932 - 947, The current check for
shouldWaitDirectTrigger relies on immediateTriggerReason matching the
direct-trigger string, but resolveImmediateTriggerReason() may return the
interval reason when info.messageCount >= messageInterval, causing direct
mentions to skip the wait; change the logic so the direct-wait path is triggered
whenever isDirectTrigger is true (and other existing guards) rather than
depending on immediateTriggerReason equality. Concretely, update the
shouldWaitDirectTrigger condition to remove the immediateTriggerReason === (...)
check (keep !session.isDirect, isDirectTrigger, copyOfConfig.messageWaitTime >
0), and when setting info.pendingDirectTrigger set reason to the explicit direct
reason (use the same string used before: 'Mention or quote trigger' or 'Nickname
trigger' based on isAppel) instead of immediateTriggerReason; this ensures
direct mentions always enter the wait path even if the interval reason was
computed first by resolveImmediateTriggerReason().

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (2)
src/plugins/filter.ts (2)

914-930: ⚠️ Potential issue | 🟠 Major

Don't let the zero-interval batching branch short-circuit unrelated triggers.

This path still runs when enableFixedIntervalTrigger is false, and the early return skips Lines 961-988 even when it is true. The result is that zero-interval groups can ignore the fixed-interval toggle and stop evaluating activity-score triggers entirely. Guard the batching path behind enableFixedIntervalTrigger and avoid returning before the independent activity-trigger flow has run.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/plugins/filter.ts` around lines 914 - 930, The zero-interval batching
branch currently runs unconditionally for group chats (checks session.isDirect
and copyOfConfig.messageInterval === 0) and returns early, preventing the
independent activity-score/fixed-interval trigger path from executing; update
the condition to guard this branch behind the feature flag
(enableFixedIntervalTrigger) so it only executes when fixed-interval triggering
is enabled, and remove or defer the early return so that the activity-trigger
flow (the logic that follows the batching block and uses activity-score) still
runs when enableFixedIntervalTrigger is false; locate the batching code using
symbols session.isDirect, copyOfConfig.messageInterval (or messageInterval),
info.pendingUserTriggers and message.id and make the guard change and
return/refactor accordingly.

932-947: ⚠️ Potential issue | 🟠 Major

Busy groups still bypass the direct wait path.

When info.messageCount >= messageInterval, resolveImmediateTriggerReason() returns the interval reason first, so shouldWaitDirectTrigger stays false and an @/nickname message replies immediately instead of waiting for the user to finish the burst.

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/plugins/filter.ts`:
- Around line 916-926: When setting info.pendingUserTriggers[message.id] you are
currently overwriting any existing direct-trigger metadata; change the write to
merge with the existing entry so an existing isDirectTrigger:true (and its
direct reason) is preserved and not downgraded by a follow-up message.
Concretely, read the prior entry (info.pendingUserTriggers[message.id]) and set
isDirectTrigger = prior?.isDirectTrigger || computedIsDirectTrigger, and keep
the prior.reason if prior.isDirectTrigger is true (otherwise use the new
computed reason); update the object creation around pendingUserTriggers to merge
rather than blindly replace.
- Around line 588-598: The current selection logic gives triggeredWakeUpReply
priority over pending user/direct waits; change it so pendingUserTrigger and
findPendingDirectTriggerReason take precedence over triggeredWakeUpReply (i.e.,
evaluate pendingUserTrigger and the result of
findPendingDirectTriggerReason(info, copyOfConfig, now, session.isDirect) first)
or explicitly suppress triggeredWakeUpReply when any wait window
(messageWaitTime/pendingUserTrigger or pending direct wait) is active; update
the expression that sets triggerReason (and any related cleanup logic that
clears pending direct waits) to only use triggeredWakeUpReply if no pending user
or direct wait reason exists.
- Around line 645-650: The deletion currently removes info.pendingUserTriggers
entries by userId unconditionally after awaiting service.triggerCollect(), which
can stomp newer triggers; before awaiting (around service.triggerCollect()),
snapshot the lastMessageAt values for each userId from
pendingUserTrigger.readyUserIds into a local map, and after the await only
delete info.pendingUserTriggers[userId] if its current lastMessageAt still
equals the snapshot value (i.e., compare against the saved timestamp) so you
only consume the exact queued entries that fired; update the code references
pendingUserTrigger, pendingUserTrigger.readyUserIds, and
info.pendingUserTriggers accordingly.

In `@src/types.ts`:
- Around line 144-147: Add a timestamp to the pendingDirectTrigger type and only
clear it when stale: update the pendingDirectTrigger definition (the object
currently with userId and reason) to include timestamp: number (same semantics
as pendingUserTriggers.lastMessageAt), set that timestamp when you
create/populate pendingDirectTrigger, and change the unconditional clearing in
processSchedulerTickForGuild to check now - pendingDirectTrigger.timestamp >=
messageWaitTime * 1000 before setting info.pendingDirectTrigger = undefined
(mirroring the stale-check used for pendingUserTriggers).

---

Duplicate comments:
In `@src/plugins/filter.ts`:
- Around line 914-930: The zero-interval batching branch currently runs
unconditionally for group chats (checks session.isDirect and
copyOfConfig.messageInterval === 0) and returns early, preventing the
independent activity-score/fixed-interval trigger path from executing; update
the condition to guard this branch behind the feature flag
(enableFixedIntervalTrigger) so it only executes when fixed-interval triggering
is enabled, and remove or defer the early return so that the activity-trigger
flow (the logic that follows the batching block and uses activity-score) still
runs when enableFixedIntervalTrigger is false; locate the batching code using
symbols session.isDirect, copyOfConfig.messageInterval (or messageInterval),
info.pendingUserTriggers and message.id and make the guard change and
return/refactor accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 768c9683-1252-4f2c-a112-8512b5def241

📥 Commits

Reviewing files that changed from the base of the PR and between c7a7aab and a41c246.

📒 Files selected for processing (4)
  • src/config.ts
  • src/plugins/filter.ts
  • src/service/trigger.ts
  • src/types.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/service/trigger.ts
  • src/config.ts

Comment thread src/plugins/filter.ts
Comment on lines 588 to +598
const triggerReason =
(triggeredWakeUpReply
? `Triggered by wake_up_reply: ${triggeredWakeUpReply.naturalReason}`
: undefined) ??
(pendingUserTrigger ? pendingUserTrigger.reason : undefined) ??
findPendingDirectTriggerReason(
info,
copyOfConfig,
now,
session.isDirect
) ??
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Wake-up replies can interrupt an active wait window.

Lines 589-591 pick triggeredWakeUpReply before the pending wait reasons. That lets a scheduled wake-up send a reply while a user is still inside messageWaitTime, which breaks the new “wait until the user stops sending” behavior; on the direct-trigger path, the success cleanup also clears the pending direct wait afterward. Pending user/direct waits should outrank wake-up replies, or wake-up replies should be suppressed while any wait window is active.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/plugins/filter.ts` around lines 588 - 598, The current selection logic
gives triggeredWakeUpReply priority over pending user/direct waits; change it so
pendingUserTrigger and findPendingDirectTriggerReason take precedence over
triggeredWakeUpReply (i.e., evaluate pendingUserTrigger and the result of
findPendingDirectTriggerReason(info, copyOfConfig, now, session.isDirect) first)
or explicitly suppress triggeredWakeUpReply when any wait window
(messageWaitTime/pendingUserTrigger or pending direct wait) is active; update
the expression that sets triggerReason (and any related cleanup logic that
clears pending direct waits) to only use triggeredWakeUpReply if no pending user
or direct wait reason exists.

Comment thread src/plugins/filter.ts
Comment on lines +645 to +650
// Consume all ready user triggers from this scheduler tick.
if (pendingUserTrigger) {
for (const userId of pendingUserTrigger.readyUserIds) {
delete info.pendingUserTriggers?.[userId]
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

wc -l src/plugins/filter.ts

Repository: ChatLunaLab/chatluna-character

Length of output: 99


🏁 Script executed:

sed -n '600,670p' src/plugins/filter.ts

Repository: ChatLunaLab/chatluna-character

Length of output: 2234


🏁 Script executed:

sed -n '550,650p' src/plugins/filter.ts

Repository: ChatLunaLab/chatluna-character

Length of output: 3039


🏁 Script executed:

rg -n 'pendingUserTrigger' src/plugins/filter.ts | head -30

Repository: ChatLunaLab/chatluna-character

Length of output: 883


🏁 Script executed:

rg -n 'findPendingUserTriggerReason|function findPendingUserTrigger' src/plugins/filter.ts -A 30 | head -80

Repository: ChatLunaLab/chatluna-character

Length of output: 2140


🏁 Script executed:

sed -n '400,430p' src/plugins/filter.ts

Repository: ChatLunaLab/chatluna-character

Length of output: 987


🏁 Script executed:

sed -n '425,445p' src/plugins/filter.ts

Repository: ChatLunaLab/chatluna-character

Length of output: 522


🏁 Script executed:

sed -n '915,945p' src/plugins/filter.ts

Repository: ChatLunaLab/chatluna-character

Length of output: 1192


🏁 Script executed:

sed -n '880,935p' src/plugins/filter.ts

Repository: ChatLunaLab/chatluna-character

Length of output: 2170


🏁 Script executed:

rg -n 'export const apply|async function apply' src/plugins/filter.ts

Repository: ChatLunaLab/chatluna-character

Length of output: 137


🏁 Script executed:

sed -n '675,740p' src/plugins/filter.ts

Repository: ChatLunaLab/chatluna-character

Length of output: 2305


🏁 Script executed:

rg -n 'middleware|ctx.on' src/plugins/filter.ts | head -20

Repository: ChatLunaLab/chatluna-character

Length of output: 135


🏁 Script executed:

sed -n '740,820p' src/plugins/filter.ts

Repository: ChatLunaLab/chatluna-character

Length of output: 2736


🏁 Script executed:

rg -n 'middleware|onMessage|ctx\.message' src/plugins/filter.ts

Repository: ChatLunaLab/chatluna-character

Length of output: 56


🏁 Script executed:

sed -n '820,900p' src/plugins/filter.ts

Repository: ChatLunaLab/chatluna-character

Length of output: 2763


🏁 Script executed:

rg -n 'service.addFilter|Context.*message' src/plugins/filter.ts | head -15

Repository: ChatLunaLab/chatluna-character

Length of output: 123


🏁 Script executed:

sed -n '1,50p' src/plugins/filter.ts

Repository: ChatLunaLab/chatluna-character

Length of output: 1462


🏁 Script executed:

rg -n 'type.*GroupInfo|interface.*GroupInfo' src/plugins/filter.ts -A 20 | head -40

Repository: ChatLunaLab/chatluna-character

Length of output: 56


🏁 Script executed:

rg -n 'pendingUserTriggers' src/types.ts -B 2 -A 2

Repository: ChatLunaLab/chatluna-character

Length of output: 248


🏁 Script executed:

sed -n '1,100p' src/types.ts

Repository: ChatLunaLab/chatluna-character

Length of output: 2825


🏁 Script executed:

sed -n '610,660p' src/plugins/filter.ts

Repository: ChatLunaLab/chatluna-character

Length of output: 1657


🏁 Script executed:

sed -n '720,790p' src/plugins/filter.ts

Repository: ChatLunaLab/chatluna-character

Length of output: 2440


Consume only the queued user entries you actually fired, not newer ones.

During the await service.triggerCollect() at line 617, incoming messages can trigger the message filter which rewrites info.pendingUserTriggers[userId] with a newer lastMessageAt. Lines 647–649 delete by userId alone without checking if the entry is still the one that triggered the response, causing newer triggers to be lost. Snapshot the lastMessageAt from pendingUserTrigger.readyUserIds before the await, then only delete entries whose timestamp still matches the snapshot.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/plugins/filter.ts` around lines 645 - 650, The deletion currently removes
info.pendingUserTriggers entries by userId unconditionally after awaiting
service.triggerCollect(), which can stomp newer triggers; before awaiting
(around service.triggerCollect()), snapshot the lastMessageAt values for each
userId from pendingUserTrigger.readyUserIds into a local map, and after the
await only delete info.pendingUserTriggers[userId] if its current lastMessageAt
still equals the snapshot value (i.e., compare against the saved timestamp) so
you only consume the exact queued entries that fired; update the code references
pendingUserTrigger, pendingUserTrigger.readyUserIds, and
info.pendingUserTriggers accordingly.

Comment thread src/plugins/filter.ts
Comment on lines +916 to +926
const reason = isDirectTrigger
? isAppel
? 'Mention or quote trigger'
: 'Nickname trigger'
: 'Message interval reached (0 mode)'
info.pendingUserTriggers = info.pendingUserTriggers ?? {}
info.pendingUserTriggers[message.id] = {
reason,
lastMessageAt: now,
isDirectTrigger
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Preserve a user's direct-trigger priority across follow-up messages.

In the common @bot + plain-text follow-up flow, the second message overwrites the same pendingUserTriggers[message.id] entry with isDirectTrigger: false and 'Message interval reached (0 mode)'. If another ready user exists, findPendingUserTriggerReason() can then stop prioritizing that explicit mention and consume it as ordinary traffic. Keep the previous direct reason/flag once a user's pending entry becomes direct.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/plugins/filter.ts` around lines 916 - 926, When setting
info.pendingUserTriggers[message.id] you are currently overwriting any existing
direct-trigger metadata; change the write to merge with the existing entry so an
existing isDirectTrigger:true (and its direct reason) is preserved and not
downgraded by a follow-up message. Concretely, read the prior entry
(info.pendingUserTriggers[message.id]) and set isDirectTrigger =
prior?.isDirectTrigger || computedIsDirectTrigger, and keep the prior.reason if
prior.isDirectTrigger is true (otherwise use the new computed reason); update
the object creation around pendingUserTriggers to merge rather than blindly
replace.

Comment thread src/types.ts
Comment on lines +144 to +147
pendingDirectTrigger?: {
userId: string
reason: string
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, examine the types.ts file around the disputed lines
cd /repo && cat -n src/types.ts | head -160 | tail -40

Repository: ChatLunaLab/chatluna-character

Length of output: 129


🏁 Script executed:

# Search for pendingDirectTrigger usage pattern
rg 'pendingDirectTrigger' -A 3 -B 3

Repository: ChatLunaLab/chatluna-character

Length of output: 2805


🏁 Script executed:

# Look for pendingUserTriggers to understand the "lastMessageAt" structure
rg 'pendingUserTriggers' -A 5 -B 2

Repository: ChatLunaLab/chatluna-character

Length of output: 4174


🏁 Script executed:

# Examine filter.ts for the clearing logic mentioned
rg -A 10 'processSchedulerTickForGuild' src/plugins/filter.ts

Repository: ChatLunaLab/chatluna-character

Length of output: 826


🏁 Script executed:

# Check for any timestamp/version fields in GroupInfo
rg 'class GroupInfo|interface GroupInfo' -A 30 src/types.ts

Repository: ChatLunaLab/chatluna-character

Length of output: 919


Add timestamp to pendingDirectTrigger to avoid dropping concurrent triggers.

pendingUserTriggers guards deletion with now - trigger.lastMessageAt >= wait * 1000 to ensure only stale entries are cleared. pendingDirectTrigger lacks this protection: after processSchedulerTickForGuild() awaits an async operation, it unconditionally clears with info.pendingDirectTrigger = undefined. If a new @/nickname arrives during the await, the unconditional clearing will discard it. Add a timestamp field (matching the pattern in pendingUserTriggers) and clear only entries older than messageWaitTime.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/types.ts` around lines 144 - 147, Add a timestamp to the
pendingDirectTrigger type and only clear it when stale: update the
pendingDirectTrigger definition (the object currently with userId and reason) to
include timestamp: number (same semantics as pendingUserTriggers.lastMessageAt),
set that timestamp when you create/populate pendingDirectTrigger, and change the
unconditional clearing in processSchedulerTickForGuild to check now -
pendingDirectTrigger.timestamp >= messageWaitTime * 1000 before setting
info.pendingDirectTrigger = undefined (mirroring the stale-check used for
pendingUserTriggers).

@dingyi222666
Copy link
Copy Markdown
Member

See #65

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants