Skip to content

Add Silent Ingest for Group Chat Messages#15841

Closed
napetrov wants to merge 1 commit intoopenclaw:mainfrom
napetrov:feat/silent-ingest
Closed

Add Silent Ingest for Group Chat Messages#15841
napetrov wants to merge 1 commit intoopenclaw:mainfrom
napetrov:feat/silent-ingest

Conversation

@napetrov
Copy link
Copy Markdown
Contributor

@napetrov napetrov commented Feb 13, 2026

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:

  • no LLM run is started,
  • no tools are invoked by core auto-reply,
  • but plugins can still observe the message through a dedicated message_ingest hook.

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 guardrails
  • shared group-policy resolver resolveChannelGroupIngest(...)
  • dedicated hook contract message_ingest in plugin types/runner
  • channel adapters (Telegram/Signal) only map inbound context into the shared path

So 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:

  • policy resolution in config/group-policy.ts (group-specific > wildcard default)
  • channel handlers stay thin and delegate cross-cutting behavior to shared helpers
  • explicit hook typing + wiring in plugin runner (message_ingest vs overloading message_received)
  • fail-open operational behavior (ingest failures/timeouts do not block message routing)

Key behavior and safety guarantees

  • Ingest runs only on mention-skip path for group messages.
  • runSilentMessageIngest enforces:
    • empty-content short-circuit,
    • hasHooks("message_ingest") short-circuit,
    • per-conversation + global inflight caps,
    • timeout wait cap,
    • inflight slot retention until hook settles (prevents timeout over-admission),
    • sender sanitization for hook event from.
  • Telegram topic isolation uses conversation key chatId:threadId.

Config support

Telegram

  • TelegramGroupConfig.ingest?: boolean
  • TelegramTopicConfig.ingest?: boolean
  • Zod schema updated for both group and topic.

Signal

  • SignalAccountConfig.groups?: Record<string, SignalGroupConfig>
  • SignalGroupConfig.ingest?: boolean
  • Zod schema updated to accept channels.signal.groups.*.ingest.

Hook contract

Adds message_ingest hook to plugin hook names/handler map and exposes runMessageIngest(...) in hook runner.

Semantics:

  • passive ingest event,
  • no automatic LLM/tool execution in this path,
  • async non-blocking from channel routing perspective.

Tests added

  • src/channels/silent-ingest.test.ts
    • sanitization path
    • no-hook short-circuit
    • timeout timer cleanup
    • inflight retention under timeout
  • src/config/group-policy.ingest.test.ts
    • group vs wildcard precedence
  • src/config/zod-schema.providers-core.telegram-ingest.test.ts
  • src/config/zod-schema.providers-core.signal-ingest.test.ts
  • src/utils/sanitize.test.ts

Practical 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: true and the message contains no mention, the new message_ingest hook fires asynchronously so plugins can observe the traffic (e.g. for memory/indexing) without paying LLM cost. The implementation is well-structured: a shared runSilentMessageIngest helper enforces per-conversation and global inflight caps, a configurable timeout, and sanitization of the from field; both the Telegram and Signal adapters delegate to it with minimal adapter-level glue.

Key points from the review:

  • Overall design is sound. The shared helper, policy resolver, and hook type wiring all follow existing patterns in the codebase. The inflight/timeout logic is correct and the test for background-hook inflight retention is a nice edge-case catch.
  • sanitizeUserText output can exceed maxLength by 3 characters — the "..." suffix is appended after slicing to maxLength, so the result is maxLength + 3 chars. This is a minor style issue, but the docstring says "limits length" which implies a strict cap.
  • Message content is not sanitized for control characters. Only the from field passes through sanitizeUserText; content receives only .trim(). Control characters (null bytes, embedded newlines, etc.) in the message body will reach message_ingest plugin 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.
  • enabled parameter is always true at both call sites. Each caller guards with if (ingestEnabled) before calling runSilentMessageIngest({ enabled: true, ... }), making the enabled parameter inside the function redundant. This is cosmetic but could cause confusion about where the guard lives.

Confidence Score: 4/5

  • This PR is safe to merge with minor style concerns; no logic or security bugs that would break existing behavior.
  • The core inflight/timeout logic is correct, the hook wiring is clean, and the change is additive (off by default, requires explicit ingest: true in config). The three findings are all style-level: (1) sanitizeUserText outputs up to maxLength + 3 chars, (2) message content isn't control-char sanitized before reaching plugins, and (3) the enabled param is always hardcoded to true at call sites. None of these break runtime behavior today.
  • src/utils/sanitize.ts (truncation length contract), src/channels/silent-ingest.ts (content sanitization gap and redundant enabled param)

Last reviewed commit: fd7ed0a

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 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".

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

16 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Feb 13, 2026

Additional Comments (1)

src/telegram/bot-message-context.ts
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.

Prompt To Fix With AI
This 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.

@napetrov
Copy link
Copy Markdown
Contributor Author

Fixed all code review issues:

P0 - Malformed import (line 58): ✅ Fixed

  • Moved getGlobalHookRunner import outside of the ./bot/helpers import block

P1 - Hook filtering not enforced (line 504/527): ✅ Fixed with dedicated hook type

  • Added new message_ingest hook type to plugin system
  • Plugins must explicitly register for message_ingest (not message_received)
  • This provides true security boundary - no accidental execution of wrong hooks
  • Removed ALLOWED_INGEST_HOOKS whitelist (no longer needed)

P1 - Zod schema missing (line 160): ✅ Fixed by simplifying config

  • Changed ingest from {enabled: boolean, hooks: string[]} to just boolean
  • Hook type itself determines what runs (not config array)
  • Simpler API, better type safety

Config Changes:
Before:

ingest:
  enabled: true
  hooks: ["session-memory"]

After:

ingest: true  # Runs all message_ingest hooks

Changes pushed in commit 7706dc7

@openclaw-barnacle openclaw-barnacle bot added channel: matrix Channel integration: matrix channel: signal Channel integration: signal channel: telegram Channel integration: telegram channel: whatsapp-web Channel integration: whatsapp-web gateway Gateway runtime cli CLI command changes agents Agent runtime and tooling size: XL size: L and removed channel: matrix Channel integration: matrix size: XL labels Feb 14, 2026
@openclaw-barnacle openclaw-barnacle bot added size: S and removed channel: whatsapp-web Channel integration: whatsapp-web gateway Gateway runtime cli CLI command changes agents Agent runtime and tooling size: L labels Feb 14, 2026
@openclaw-barnacle openclaw-barnacle bot added the agents Agent runtime and tooling label Feb 16, 2026
@openclaw-barnacle openclaw-barnacle bot removed the agents Agent runtime and tooling label Feb 16, 2026
@openclaw-barnacle openclaw-barnacle bot added size: M agents Agent runtime and tooling and removed size: S labels Feb 16, 2026
@napetrov
Copy link
Copy Markdown
Contributor Author

if you enable silent ingest for session-memory or command-logger, those handlers will run on skipped group messages

but right now, it sends the event into the plugin message_received pipeline instead, so the two named handlers never run, no?

also, the PR added ingest to TypeScript/runtime types, but not to the strict config schema, so ingest keys would get rejected as invalid config and won't run.

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

@openclaw-barnacle openclaw-barnacle bot added the cli CLI command changes label Mar 20, 2026
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 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;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

Comment on lines +299 to +301
const validHooks = hooks.filter((h: string): h is string =>
ALLOWED_INGEST_HOOKS.includes(h as (typeof ALLOWED_INGEST_HOOKS)[number]),
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

Comment on lines +323 to +327
const timeoutPromise = new Promise<void>((_, reject) => {
timeoutHandle = setTimeout(() => reject(new Error("Hook timeout")), HOOK_TIMEOUT_MS);
});
void Promise.race([
hookRunner.runMessageIngest(
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 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".

Comment on lines +283 to +287
const { groupConfig: resolvedGroupConfig, defaultConfig: wildcardGroupConfig } =
resolveChannelGroupPolicy({
cfg,
channel: "telegram",
groupId: String(chatId),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Comment on lines +75 to +78
.object({
enabled: z.boolean(),
hooks: z.array(z.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.

P2 Badge 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 👍 / 👎.

@napetrov napetrov force-pushed the feat/silent-ingest branch from bc07f9c to 451a546 Compare March 23, 2026 16:12
@openclaw-barnacle openclaw-barnacle bot removed the docs Improvements or additions to documentation label Mar 23, 2026
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 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".

Comment on lines +307 to +308
const messageIdForHook =
typeof msg.message_id === "number" ? String(msg.message_id) : 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.

P2 Badge 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 👍 / 👎.

@openclaw-barnacle openclaw-barnacle bot added the docs Improvements or additions to documentation label Mar 23, 2026
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 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}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

@napetrov napetrov force-pushed the feat/silent-ingest branch from 6baaad3 to e3388ba Compare April 2, 2026 18:01
@openclaw-barnacle openclaw-barnacle bot added channel: discord Channel integration: discord channel: line Channel integration: line and removed cli CLI command changes labels Apr 2, 2026
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 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",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

Comment on lines +195 to +197
if (arg.startsWith("-")) {
optionArgs.push(arg);
consumeNextAsOptionValue = !arg.includes("=") && OPTION_TAKES_VALUE.has(arg);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 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".

Comment on lines +781 to +783
void Promise.race([
hookRunner.runMessageIngestForPlugin(pluginId, ingestEvent, ingestCtx),
timeoutPromise,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 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".

Comment on lines +300 to +304
? (params.groupHistory ?? params.groupHistories.get(params.groupHistoryKey) ?? []).map(
(entry) => ({
sender: entry.sender,
body: entry.body,
timestamp: entry.timestamp,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 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",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

@napetrov
Copy link
Copy Markdown
Contributor Author

napetrov commented Apr 3, 2026

CI is green now 🟢 — ready for review @obviyus

@obviyus
Copy link
Copy Markdown
Contributor

obviyus commented Apr 3, 2026

Thanks @napetrov! Will review in a bit. Don't worry about unrelated CI breakage though.

@napetrov
Copy link
Copy Markdown
Contributor Author

napetrov commented Apr 3, 2026

@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 message_ingest was added to PluginHookName in src/plugins/types.ts and the hook runner dispatches via runMessageIngestForPluginrunVoidHookForPlugin("message_ingest", ...) — completely separate from message_received. The tests in src/plugins/wired-hooks-message-ingest.test.ts verify isolation (ingest doesn't fire message_received and vice versa).

For session-memory / command-logger to receive these events, they'd need to register a message_ingest handler in their plugin definition — that's intentional follow-up work per plugin, not part of this infrastructure PR.

2. "ingest not in strict config schema"

Also fixed in 33b2630IngestConfigSchema was added to TelegramGroupSchema, TelegramTopicSchema, and the Signal group schema in zod-schema.providers-core.ts. Just pushed another commit (2965452) that also makes the hook array strictly typed as AllowedIngestHook[] (enum-validated in Zod too via z.array(z.enum(ALLOWED_INGEST_HOOKS))).

Let me know if anything else needs clarification!

@obviyus
Copy link
Copy Markdown
Contributor

obviyus commented Apr 3, 2026

@napetrov can you take a look at #60018? Far smaller surface for the same bug. If you can remove all the CI changed you had to do I'm happy to land your PR instead. As it stands right now, this PR requires the secops approval so I can't land it anyway.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 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".

Comment on lines +70 to +71
const convInflight = inflightByConversation.get(conversationId) ?? 0;
if (inflightGlobal >= MAX_INGEST_INFLIGHT_GLOBAL) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 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".

Comment on lines +43 to +45
return candidate.hooks.filter((hook): hook is AllowedIngestHook =>
ALLOWED_INGEST_HOOKS.includes(hook),
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

@napetrov
Copy link
Copy Markdown
Contributor Author

napetrov commented Apr 3, 2026

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 message_ingest hook + ingest-runtime SDK subpath, switch to triggerInternalHook, simplify config to ingest: boolean).

What's your preference — close this in favour of #60018, or rework it here?

@obviyus
Copy link
Copy Markdown
Contributor

obviyus commented Apr 3, 2026

Sounds good, I'll proceed with #60081 then.

@obviyus
Copy link
Copy Markdown
Contributor

obviyus commented Apr 3, 2026

Closing as duplicate; this was superseded by #60018.

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

Labels

channel: signal Channel integration: signal channel: telegram Channel integration: telegram scripts Repository scripts size: L stale Marked as stale due to inactivity

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] Native listen-only mode: receive all messages without LLM invocation

2 participants