Add Silent Ingest for Group Chat Messages#15841
Add Silent Ingest for Group Chat Messages#15841napetrov wants to merge 1 commit intoopenclaw:mainfrom
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1fc91e7b6b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Additional Comments (1)
Also note Fix by moving Prompt To Fix With AIThis is a comment left during a code review.
Path: src/telegram/bot-message-context.ts
Line: 53:60
Comment:
**Broken import block**
`getGlobalHookRunner` is currently inserted inside the named import list from `./bot/helpers.js`, which makes this file syntactically invalid (it’s missing the closing `}` before starting a new `import`). This will fail parsing/compilation.
Also note `conversationLabel` is referenced in the ingest block before it’s declared later in the function, which is another compile-time error.
Fix by moving `getGlobalHookRunner` to its own top-level import and ensuring `conversationLabel` (or an equivalent string) is defined before use in the ingest path.
How can I resolve this? If you propose a fix, please make it concise. |
|
Fixed all code review issues: P0 - Malformed import (line 58): ✅ Fixed
P1 - Hook filtering not enforced (line 504/527): ✅ Fixed with dedicated hook type
P1 - Zod schema missing (line 160): ✅ Fixed by simplifying config
Config Changes: ingest:
enabled: true
hooks: ["session-memory"]After: ingest: true # Runs all message_ingest hooksChanges pushed in commit 7706dc7 |
fcd12c2 to
baa6d7f
Compare
bfc1ccb to
f92900f
Compare
42cad5c to
2f4c0f3
Compare
1fcab0c to
85ceebe
Compare
thanks, it failed apart with all those automatic rebases and fixes through last 3 weeks. Let me fix it to original intent and thanks for catching |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 341c043f4b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| * SECURITY: Only add hooks that are safe to run on untrusted/public messages. | ||
| * Hooks listed here will process messages without user mention or LLM oversight. | ||
| */ | ||
| export const ALLOWED_INGEST_HOOKS = ["session-memory", "command-logger"] as const; |
There was a problem hiding this comment.
Route ingest config to handlers that can actually execute
Fresh evidence in this revision is src/plugins/wired-hooks-message-ingest.test.ts:14-46, which shows silent ingest only dispatches typed message_ingest plugin hooks. The whitelist here still only allows session-memory and command-logger, but those in-tree names are bundled internal hooks loaded via registerInternalHook (src/hooks/loader.ts:136-145), and their handlers immediately return unless event.type === "command" (src/hooks/bundled/session-memory/handler.ts:199-203, src/hooks/bundled/command-logger/handler.ts:38-41). Because the Telegram/Signal adapters call hookRunner.runMessageIngest(...) instead of triggerInternalHook(...), a config that follows this whitelist never runs an ingest handler at all.
Useful? React with 👍 / 👎.
| const validHooks = hooks.filter((h: string): h is string => | ||
| ALLOWED_INGEST_HOOKS.includes(h as (typeof ALLOWED_INGEST_HOOKS)[number]), | ||
| ); |
There was a problem hiding this comment.
Respect ingest.hooks when dispatching message_ingest
Fresh evidence in this revision is the new runMessageIngest() path in src/plugins/hooks.ts:598-602: it delegates to runVoidHook(), and runVoidHook() only filters registrations by hook name (src/plugins/hooks.ts:139-145, src/plugins/hooks.ts:247-262). Here we compute validHooks, but never use those IDs when dispatching, so once a group lists any allowed hook name every installed message_ingest plugin still runs. In a deployment with multiple ingest plugins, channels.*.groups.*.ingest.hooks = ["session-memory"] would still expose skipped-group traffic to unrelated handlers.
Useful? React with 👍 / 👎.
| const timeoutPromise = new Promise<void>((_, reject) => { | ||
| timeoutHandle = setTimeout(() => reject(new Error("Hook timeout")), HOOK_TIMEOUT_MS); | ||
| }); | ||
| void Promise.race([ | ||
| hookRunner.runMessageIngest( |
There was a problem hiding this comment.
Cap timed-out ingest jobs instead of letting them pile up
The 5s timeout here only rejects the local Promise.race; it does not stop hookRunner.runMessageIngest(...). Because this new mention-skip path launches a fresh background ingest for every skipped message and there is no shared inflight counter or dedupe in either adapter, a slow or hung message_ingest plugin on a busy group will accumulate arbitrarily many concurrent jobs even though each caller logs a timeout. That turns high-volume mention-gated chats into unbounded CPU/memory work.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bc07f9c8f7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const { groupConfig: resolvedGroupConfig, defaultConfig: wildcardGroupConfig } = | ||
| resolveChannelGroupPolicy({ | ||
| cfg, | ||
| channel: "telegram", | ||
| groupId: String(chatId), |
There was a problem hiding this comment.
Read Telegram ingest settings from the fresh config
buildTelegramMessageContext() already reloads Telegram group/topic config on each message via resolveTelegramGroupConfig() in extensions/telegram/src/bot.ts:445-469, so operators can change per-group behavior without restarting. This new branch bypasses those fresh groupConfig/topicConfig values and recomputes ingestConfig from the older cfg snapshot instead. In practice, changing channels.telegram.groups.*.ingest (or a wildcard fallback) at runtime will not take effect for mention-skipped messages until the gateway restarts.
Useful? React with 👍 / 👎.
| .object({ | ||
| enabled: z.boolean(), | ||
| hooks: z.array(z.string()), | ||
| }) |
There was a problem hiding this comment.
Validate ingest hook IDs in the config schema
IngestConfigSchema accepts any string here, but both channel adapters later drop entries that are not in ALLOWED_INGEST_HOOKS (extensions/telegram/src/bot-message-context.body.ts:299-301 and extensions/signal/src/monitor/event-handler.ts:725-729). That means a typo or unsupported hook ID parses cleanly and silently disables silent ingest for that group, which is a hard-to-debug production misconfiguration. The schema should reject unsupported values (or emit a config diagnostic) instead of accepting them and no-oping at runtime.
Useful? React with 👍 / 👎.
bc07f9c to
451a546
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 873306b3b1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const messageIdForHook = | ||
| typeof msg.message_id === "number" ? String(msg.message_id) : undefined; |
There was a problem hiding this comment.
Use overridden Telegram message ID for silent ingest
In buffered/debounced Telegram group messages, processMessage can pass options.messageIdOverride from the last fragment (extensions/telegram/src/bot-handlers.buffers.ts:249-250, extensions/telegram/src/bot-handlers.runtime.ts:250-258), and the normal inbound path preserves that via MessageSid (extensions/telegram/src/bot-message-context.session.ts:210). This ingest branch always falls back to msg.message_id, so message_ingest plugins receive the synthetic base message's ID instead of the canonical merged-message ID. Plugins that dedupe or correlate on metadata.messageId will therefore treat mention-skipped merged messages differently from normal inbound messages in the same chat.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6baaad3f65
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| {"recordType":"path","path":"channels.discord.voice.tts.provider","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} | ||
| {"recordType":"path","path":"channels.discord.voice.tts.summaryModel","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} | ||
| {"recordType":"path","path":"channels.discord.voice.tts.timeoutMs","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} | ||
| {"recordType":"path","path":"channels.discord","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Discord","help":"very well supported right now.","hasChildren":false} |
There was a problem hiding this comment.
Rebuild the config baseline without dropping channel subtrees
I checked the regenerated docs/.generated/config-baseline.jsonl and this line now marks channels.discord as hasChildren: false, with every channels.discord.* entry removed, even though this PR does not touch the Discord schema (src/config/zod-schema.providers-core.ts:403-685 still defines it). The same regen also drops totalPaths from 5628 to 3494, so the committed baseline appears to have been produced from an incomplete schema set and will publish incomplete config docs/drift artifacts for unrelated channels.
Useful? React with 👍 / 👎.
6baaad3 to
e3388ba
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e3388ba117
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
package.json
Outdated
| "zod": "^4.3.6" | ||
| }, | ||
| "devDependencies": { | ||
| "@anthropic-ai/vertex-sdk": "^0.14.4", |
There was a problem hiding this comment.
Keep @anthropic-ai/vertex-sdk in runtime dependencies
This move puts @anthropic-ai/vertex-sdk under devDependencies, but src/agents/anthropic-vertex-stream.ts imports it in production code at module load time. Installs that omit dev deps (for example production/global installs) will no longer include the SDK, so selecting Anthropic Vertex models will fail with a module-resolution error at runtime.
Useful? React with 👍 / 👎.
scripts/test-parallel.mjs
Outdated
| if (arg.startsWith("-")) { | ||
| optionArgs.push(arg); | ||
| consumeNextAsOptionValue = !arg.includes("=") && OPTION_TAKES_VALUE.has(arg); |
There was a problem hiding this comment.
Parse wrapper --profile/--surface flags before passthrough
This parser now treats all unknown --* args as Vitest passthrough and only binds a following value for flags listed in OPTION_TAKES_VALUE; since wrapper flags like --profile/--surface are not handled, commands such as test:max and test:channels no longer drive wrapper behavior and instead forward those flags while misclassifying max/channels as file filters. That breaks existing package scripts that depend on wrapper-level lane selection.
Useful? React with 👍 / 👎.
| // Use canonical IDs matching the normal message pipeline | ||
| const canonicalTo = `telegram:${chatId}`; | ||
| const canonicalFrom = buildTelegramGroupFrom(chatId, resolvedThreadId); | ||
| const ingestConversationId = buildTelegramGroupPeerId(chatId, resolvedThreadId); |
There was a problem hiding this comment.
Canonicalize Telegram ingest conversationId
message_ingest context uses buildTelegramGroupPeerId(...) here, producing raw chat/topic IDs, while the same event already sets canonical originatingTo (telegram:<chatId>) and normal message_received hooks derive conversationId from canonical OriginatingTo. In chats that receive both skipped-ingest and normal messages, plugins that bucket state by ctx.conversationId will split one conversation into two identifiers.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 069e3be7a7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| void Promise.race([ | ||
| hookRunner.runMessageIngestForPlugin(pluginId, ingestEvent, ingestCtx), | ||
| timeoutPromise, |
There was a problem hiding this comment.
Bound concurrent Signal silent-ingest hook executions
This dispatch uses Promise.race with a timeout but never limits how many background runMessageIngestForPlugin(...) jobs can exist at once. In a mention-gated group with high traffic and a slow or hung ingest plugin, every skipped message still spawns another hook job, so concurrency can grow without bound even though callers time out after 5 seconds. That can drive sustained CPU/memory pressure; add shared inflight caps/backpressure before launching the hook.
Useful? React with 👍 / 👎.
| }; | ||
| const ingestEvent = { | ||
| from: canonicalFrom, | ||
| content: rawBody, |
There was a problem hiding this comment.
Send resolved Telegram body text to message_ingest
The ingest payload uses rawBody, which for audio-only messages is typically a media placeholder, even when preflight transcription already produced a richer bodyText earlier in this function. In mention-skipped audio group messages, plugins therefore receive placeholder content instead of the available transcript, reducing indexing/memory quality and making ingest content inconsistent with the normal inbound body resolution path.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d20707a354
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| ? (params.groupHistory ?? params.groupHistories.get(params.groupHistoryKey) ?? []).map( | ||
| (entry) => ({ | ||
| sender: entry.sender, | ||
| body: entry.body, | ||
| timestamp: entry.timestamp, |
There was a problem hiding this comment.
Restore allowlist filtering for WhatsApp group history context
This now builds InboundHistory straight from groupHistories without running the existing visibility filter (resolveVisibleWhatsAppGroupHistory) or contextVisibility policy checks. In a group configured with groupPolicy: "allowlist" and contextVisibility: "allowlist", messages from non-allowlisted senders will be included in model context, which is a regression from the prior behavior enforced in extensions/whatsapp/src/auto-reply/monitor/inbound-context.ts.
Useful? React with 👍 / 👎.
| msg: params.msg, | ||
| senderE164: sender.e164 ?? undefined, | ||
| normalizeE164, | ||
| const replyTo = getReplyContext(params.msg); |
There was a problem hiding this comment.
Reapply quote visibility checks before using WhatsApp reply context
The reply target is now taken directly from getReplyContext(params.msg) and then forwarded into ReplyToBody/ReplyToSender, bypassing resolveVisibleWhatsAppReplyContext. That removes the allowlist vs allowlist_quote enforcement for quoted senders, so in allowlist mode a quote from a blocked participant can leak into prompt context instead of being redacted.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6996af92f2
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| "@agentclientprotocol/sdk": "0.17.1", | ||
| "@anthropic-ai/vertex-sdk": "^0.14.4", | ||
| "@aws-sdk/client-bedrock": "3.1020.0", | ||
| "@clack/prompts": "^1.1.0", |
There was a problem hiding this comment.
Restore Bedrock SDK mirror in root dependencies
Removing @aws-sdk/client-bedrock from the root runtime dependency list breaks the bundled Bedrock plugin mirror contract: extensions/amazon-bedrock/package.json declares openclaw.releaseChecks.rootDependencyMirrorAllowlist for this package, and scripts/release-check.ts explicitly treats missing root mirrors as an error. This means release/package validation will fail, and packed artifacts can miss the runtime dependency expected by Bedrock staging checks (src/plugins/contracts/plugin-sdk-package-contract-guardrails.test.ts).
Useful? React with 👍 / 👎.
|
CI is green now 🟢 — ready for review @obviyus |
|
Thanks @napetrov! Will review in a bit. Don't worry about unrelated CI breakage though. |
|
@obviyus thanks for the review! Addressing both points: 1. "sends the event into the plugin message_received pipeline" This was fixed in commit 33b2630 (merged into this PR). The hook name For 2. " Also fixed in 33b2630 — Let me know if anything else needs clarification! |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2965452dfe
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const convInflight = inflightByConversation.get(conversationId) ?? 0; | ||
| if (inflightGlobal >= MAX_INGEST_INFLIGHT_GLOBAL) { |
There was a problem hiding this comment.
Enforce ingest inflight caps per plugin dispatch
The throttling checks run only once before iterating validHooks, so one skipped message can still overrun the configured limits when multiple plugin IDs are listed. For example, with conversation inflight at 7 (cap 8) and two configured hooks, both iterations will dispatch and increment, reaching 9 inflight jobs for that conversation. This weakens the backpressure guarantees and can still amplify load under bursty traffic; re-check (or reserve) capacity before each incrementInflight inside the loop.
Useful? React with 👍 / 👎.
…ages Implements passive message_ingest hook path for group messages skipped by mention-gating (requireMention: true). When a non-mentioned group message is skipped: - no LLM run is started - plugins registered for message_ingest can observe the message - no tools invoked, no auto-reply sent Key changes: - src/config/ingest-hooks.ts: ALLOWED_INGEST_HOOKS allowlist + IngestConfig type - src/plugin-sdk/ingest-runtime.ts: shared dispatchSilentMessageIngest helper with per-conversation (8) and global (64) inflight caps + 5s timeout - src/plugins/types.ts + hooks.ts: message_ingest hook type and runner - src/config/group-policy.ts + types: ingest field in ChannelGroupConfig - src/config/zod-schema.providers-core.ts: IngestConfigSchema in Telegram/Signal - src/utils/sanitize.ts: sanitizeUserText for metadata fields - extensions/telegram/src/bot-message-context.body.ts: ingest dispatch on mention-skip - extensions/signal/src/monitor/event-handler.ts: ingest dispatch on mention-skip - Tests: behavioral tests for Telegram, Signal, hook runner, zod schema Config example: channels.telegram.groups.-100123.ingest.enabled: true channels.telegram.groups.-100123.ingest.hooks: [session-memory] Fixes openclaw#15268
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0a7f84effc
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| return candidate.hooks.filter((hook): hook is AllowedIngestHook => | ||
| ALLOWED_INGEST_HOOKS.includes(hook), | ||
| ); |
There was a problem hiding this comment.
Deduplicate configured ingest hook IDs before dispatch
resolveConfiguredIngestHooks() preserves duplicate entries from ingest.hooks, so a config like hooks: ["session-memory", "session-memory"] will execute runMessageIngestForPlugin twice for the same plugin on every skipped message. That can duplicate memory/index records and artificially inflate inflight counters/cap hits even though only one plugin was intended; the hook list should be normalized to unique IDs (or rejected in schema) before dispatch.
Useful? React with 👍 / 👎.
|
Thanks for the pointer @obviyus! Took a look at #60018 — the approach makes sense: reuse the existing internal hook infrastructure instead of a new SDK subpath, keep config as a simple boolean. I'd be happy to close this PR in favour of #60018 if that's the path you want to land. Alternatively, I can rework this PR to match that minimal surface (drop the new What's your preference — close this in favour of #60018, or rework it here? |
|
Sounds good, I'll proceed with #60081 then. |
|
Closing as duplicate; this was superseded by #60018. |
Add Silent Ingest for Mention-Gated Group Chats
Fixes #15268
What this PR does
Implements a native silent ingest path for group messages that are skipped by mention-gating (
requireMention: true).When a non-mentioned group message is skipped:
message_ingesthook.This removes the old "NO_REPLY prompt workaround" cost pattern and makes the behavior explicit in config/runtime.
Why this is now more generalized (not channel-specific glue)
This PR started as channel-local logic and was refactored into a shared pipeline:
runSilentMessageIngest(...)shared helper with common guardrailsresolveChannelGroupIngest(...)message_ingestin plugin types/runnerSo the model is now platform-aligned and extendable to other group-capable channels without re-implementing safety logic.
Design alignment with current OpenClaw architecture
The implementation follows existing OpenClaw patterns:
config/group-policy.ts(group-specific > wildcard default)message_ingestvs overloadingmessage_received)Key behavior and safety guarantees
runSilentMessageIngestenforces:hasHooks("message_ingest")short-circuit,from.chatId:threadId.Config support
Telegram
TelegramGroupConfig.ingest?: booleanTelegramTopicConfig.ingest?: booleanSignal
SignalAccountConfig.groups?: Record<string, SignalGroupConfig>SignalGroupConfig.ingest?: booleanchannels.signal.groups.*.ingest.Hook contract
Adds
message_ingesthook to plugin hook names/handler map and exposesrunMessageIngest(...)in hook runner.Semantics:
Tests added
src/channels/silent-ingest.test.tssrc/config/group-policy.ingest.test.tssrc/config/zod-schema.providers-core.telegram-ingest.test.tssrc/config/zod-schema.providers-core.signal-ingest.test.tssrc/utils/sanitize.test.tsPractical impact
For busy group chats, this enables memory/indexing hooks on non-mentioned traffic without paying per-message LLM cost, while keeping responses mention-gated and consistent with existing group policy behavior.
Greptile Summary
This PR adds a silent ingest path for group messages that are skipped by mention-gating. When
requireMention: trueand the message contains no mention, the newmessage_ingesthook fires asynchronously so plugins can observe the traffic (e.g. for memory/indexing) without paying LLM cost. The implementation is well-structured: a sharedrunSilentMessageIngesthelper enforces per-conversation and global inflight caps, a configurable timeout, and sanitization of thefromfield; both the Telegram and Signal adapters delegate to it with minimal adapter-level glue.Key points from the review:
sanitizeUserTextoutput can exceedmaxLengthby 3 characters — the"..."suffix is appended after slicing tomaxLength, so the result ismaxLength + 3chars. This is a minor style issue, but the docstring says "limits length" which implies a strict cap.fromfield passes throughsanitizeUserText;contentreceives only.trim(). Control characters (null bytes, embedded newlines, etc.) in the message body will reachmessage_ingestplugin handlers unchanged. This appears to be an intentional design choice (the PR description explicitly calls out "sender sanitization"), but it is worth a second look for log-injection safety in plugins.enabledparameter is alwaystrueat both call sites. Each caller guards withif (ingestEnabled)before callingrunSilentMessageIngest({ enabled: true, ... }), making theenabledparameter inside the function redundant. This is cosmetic but could cause confusion about where the guard lives.Confidence Score: 4/5
ingest: truein config). The three findings are all style-level: (1)sanitizeUserTextoutputs up tomaxLength + 3chars, (2) message content isn't control-char sanitized before reaching plugins, and (3) theenabledparam is always hardcoded totrueat call sites. None of these break runtime behavior today.enabledparam)Last reviewed commit: fd7ed0a