feat: add group chat message wait time#66
Conversation
📝 WalkthroughWalkthroughAdds 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
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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.
| for (let retryCount = 0; retryCount < 2; retryCount++) { | ||
| if (signal?.aborted) return | ||
| let emittedAny = false | ||
| if (signal?.aborted) return |
| info.pendingDirectTrigger = undefined | ||
| info.pendingNextReplies = [] |
| return | ||
| } | ||
| service.mute(session, copyOfConfig.muteTime * 1000) | ||
| return |
| // For group chats with messageInterval === 0, don't trigger immediately | ||
| // Instead, handle via pendingUserTriggers in the scheduler | ||
| return undefined |
| 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 | ||
| } |
There was a problem hiding this comment.
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 | 🟡 MinorUpdate the
messageInterval = 0help 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
messageWaitTimebefore 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
📒 Files selected for processing (5)
src/config.tssrc/plugins/chat.tssrc/plugins/filter.tssrc/service/trigger.tssrc/types.ts
| 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) | ||
| } |
There was a problem hiding this comment.
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.
| // 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 | ||
| } |
There was a problem hiding this comment.
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.
| // 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.
| 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 | ||
| } |
There was a problem hiding this comment.
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.
| 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().
There was a problem hiding this comment.
Actionable comments posted: 4
♻️ Duplicate comments (2)
src/plugins/filter.ts (2)
914-930:⚠️ Potential issue | 🟠 MajorDon't let the zero-interval batching branch short-circuit unrelated triggers.
This path still runs when
enableFixedIntervalTriggeris false, and the earlyreturnskips 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 behindenableFixedIntervalTriggerand 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 | 🟠 MajorBusy groups still bypass the direct wait path.
When
info.messageCount >= messageInterval,resolveImmediateTriggerReason()returns the interval reason first, soshouldWaitDirectTriggerstays false and an@/nicknamemessage 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
📒 Files selected for processing (4)
src/config.tssrc/plugins/filter.tssrc/service/trigger.tssrc/types.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- src/service/trigger.ts
- src/config.ts
| const triggerReason = | ||
| (triggeredWakeUpReply | ||
| ? `Triggered by wake_up_reply: ${triggeredWakeUpReply.naturalReason}` | ||
| : undefined) ?? | ||
| (pendingUserTrigger ? pendingUserTrigger.reason : undefined) ?? | ||
| findPendingDirectTriggerReason( | ||
| info, | ||
| copyOfConfig, | ||
| now, | ||
| session.isDirect | ||
| ) ?? |
There was a problem hiding this comment.
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.
| // Consume all ready user triggers from this scheduler tick. | ||
| if (pendingUserTrigger) { | ||
| for (const userId of pendingUserTrigger.readyUserIds) { | ||
| delete info.pendingUserTriggers?.[userId] | ||
| } | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
wc -l src/plugins/filter.tsRepository: ChatLunaLab/chatluna-character
Length of output: 99
🏁 Script executed:
sed -n '600,670p' src/plugins/filter.tsRepository: ChatLunaLab/chatluna-character
Length of output: 2234
🏁 Script executed:
sed -n '550,650p' src/plugins/filter.tsRepository: ChatLunaLab/chatluna-character
Length of output: 3039
🏁 Script executed:
rg -n 'pendingUserTrigger' src/plugins/filter.ts | head -30Repository: ChatLunaLab/chatluna-character
Length of output: 883
🏁 Script executed:
rg -n 'findPendingUserTriggerReason|function findPendingUserTrigger' src/plugins/filter.ts -A 30 | head -80Repository: ChatLunaLab/chatluna-character
Length of output: 2140
🏁 Script executed:
sed -n '400,430p' src/plugins/filter.tsRepository: ChatLunaLab/chatluna-character
Length of output: 987
🏁 Script executed:
sed -n '425,445p' src/plugins/filter.tsRepository: ChatLunaLab/chatluna-character
Length of output: 522
🏁 Script executed:
sed -n '915,945p' src/plugins/filter.tsRepository: ChatLunaLab/chatluna-character
Length of output: 1192
🏁 Script executed:
sed -n '880,935p' src/plugins/filter.tsRepository: ChatLunaLab/chatluna-character
Length of output: 2170
🏁 Script executed:
rg -n 'export const apply|async function apply' src/plugins/filter.tsRepository: ChatLunaLab/chatluna-character
Length of output: 137
🏁 Script executed:
sed -n '675,740p' src/plugins/filter.tsRepository: ChatLunaLab/chatluna-character
Length of output: 2305
🏁 Script executed:
rg -n 'middleware|ctx.on' src/plugins/filter.ts | head -20Repository: ChatLunaLab/chatluna-character
Length of output: 135
🏁 Script executed:
sed -n '740,820p' src/plugins/filter.tsRepository: ChatLunaLab/chatluna-character
Length of output: 2736
🏁 Script executed:
rg -n 'middleware|onMessage|ctx\.message' src/plugins/filter.tsRepository: ChatLunaLab/chatluna-character
Length of output: 56
🏁 Script executed:
sed -n '820,900p' src/plugins/filter.tsRepository: ChatLunaLab/chatluna-character
Length of output: 2763
🏁 Script executed:
rg -n 'service.addFilter|Context.*message' src/plugins/filter.ts | head -15Repository: ChatLunaLab/chatluna-character
Length of output: 123
🏁 Script executed:
sed -n '1,50p' src/plugins/filter.tsRepository: ChatLunaLab/chatluna-character
Length of output: 1462
🏁 Script executed:
rg -n 'type.*GroupInfo|interface.*GroupInfo' src/plugins/filter.ts -A 20 | head -40Repository: ChatLunaLab/chatluna-character
Length of output: 56
🏁 Script executed:
rg -n 'pendingUserTriggers' src/types.ts -B 2 -A 2Repository: ChatLunaLab/chatluna-character
Length of output: 248
🏁 Script executed:
sed -n '1,100p' src/types.tsRepository: ChatLunaLab/chatluna-character
Length of output: 2825
🏁 Script executed:
sed -n '610,660p' src/plugins/filter.tsRepository: ChatLunaLab/chatluna-character
Length of output: 1657
🏁 Script executed:
sed -n '720,790p' src/plugins/filter.tsRepository: 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.
| 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 | ||
| } |
There was a problem hiding this comment.
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.
| pendingDirectTrigger?: { | ||
| userId: string | ||
| reason: string | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, examine the types.ts file around the disputed lines
cd /repo && cat -n src/types.ts | head -160 | tail -40Repository: ChatLunaLab/chatluna-character
Length of output: 129
🏁 Script executed:
# Search for pendingDirectTrigger usage pattern
rg 'pendingDirectTrigger' -A 3 -B 3Repository: ChatLunaLab/chatluna-character
Length of output: 2805
🏁 Script executed:
# Look for pendingUserTriggers to understand the "lastMessageAt" structure
rg 'pendingUserTriggers' -A 5 -B 2Repository: ChatLunaLab/chatluna-character
Length of output: 4174
🏁 Script executed:
# Examine filter.ts for the clearing logic mentioned
rg -A 10 'processSchedulerTickForGuild' src/plugins/filter.tsRepository: 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.tsRepository: 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).
|
See #65 |
Summary
引入群聊消息等待时间能力,减少用户连续发送多条消息时被过早打断的问题。
Changes
messageWaitTime,默认值为0messageInterval = 0时,不再收到消息就立刻触发,而是等待同一用户在指定时间内不再继续发送消息后再回复@、引用或昵称触发时,也支持按messageWaitTime延迟触发,避免用户消息尚未发完就开始回复Impact
messageWaitTime > 0后,群聊回复会更贴近“等用户说完再答”的交互预期Summary by CodeRabbit
New Features
Improvements