Skip to content

fix(hooks): bridge message_sending event to internal hook registry#73570

Open
SymbolStar wants to merge 6 commits into
openclaw:mainfrom
SymbolStar:fix/73542-message-sending-internal-hook
Open

fix(hooks): bridge message_sending event to internal hook registry#73570
SymbolStar wants to merge 6 commits into
openclaw:mainfrom
SymbolStar:fix/73542-message-sending-internal-hook

Conversation

@SymbolStar

Copy link
Copy Markdown
Contributor

Root Cause

The message_sending event was dispatched only through the plugin hook runner (hookRunner.runMessageSending()) but was never bridged to the internal hook registry via triggerInternalHook(). This meant user-defined hooks (registered in ~/.openclaw/hooks/) listening for message_sending were never invoked.

In contrast, message_sent correctly calls both paths: the plugin hook runner AND triggerInternalHook(createInternalHookEvent("message", "sent", ...)).

Fix

Add a triggerInternalHook(createInternalHookEvent("message", "sending", ...)) call in deliverOutboundPayloads, immediately after applyMessageSendingHook() returns, mirroring the existing message_sent pattern.

The internal hook is fire-and-forget (notification only) since triggerInternalHook returns Promise<void> — content modification still goes through the plugin hook runner path. User-defined hooks will now receive the event notification as expected.

Verification

  • Single file change, 22 lines added
  • Mirrors the proven message_sent bridging pattern at line ~667
  • CI will validate via existing hook dispatch tests

Fixes #73542

@greptile-apps

greptile-apps Bot commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR bridges the message_sending event to the internal hook registry, mirroring the existing message_sent pattern. The core approach is correct, but the new block has two defects in how it constructs and gates the event.

Confidence Score: 3/5

Not safe to merge — two P1 defects mean internal hook subscribers receive wrong content and spurious events for cancelled sends.

Two independent P1 defects: (1) the hook event always carries pre-plugin-modification content because the outer payloadSummary is used instead of hookResult.payloadSummary, and (2) the hook fires even when the send is cancelled because the guard is placed after the fireAndForgetHook call. Both affect the correctness of the new feature on its primary code path.

src/infra/outbound/deliver.ts — specifically the new block around lines 1032–1053.

Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/infra/outbound/deliver.ts
Line: 1041

Comment:
**Stale payload content sent to internal hook**

The internal hook event is built from the outer `payloadSummary` variable (captured before `applyMessageSendingHook` ran), not from `hookResult.payloadSummary`. When a plugin hook modifies the message content (`sendingResult.content != null`), `applyMessageSendingHook` returns an updated `payloadSummary` in `hookResult.payloadSummary` — but that modified content is never used here. Internal hook subscribers will always see the pre-plugin-modification content, even when a plugin rewrote it.

```suggestion
                content: hookResult.payloadSummary.hookContent ?? hookResult.payloadSummary.text,
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/infra/outbound/deliver.ts
Line: 1032-1053

Comment:
**Internal hook fires for cancelled sends**

The `fireAndForgetHook` call happens unconditionally before the `if (hookResult.cancelled) { continue; }` guard on line 1054. When a plugin hook sets `cancel: true`, the internal `message_sending` hook fires anyway — telling user-defined hooks that a message is being sent when it will actually never leave the system. Add a `!hookResult.cancelled` guard to the condition.

```suggestion
      if (sessionKeyForInternalHooks && !hookResult.cancelled) {
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix(hooks): bridge message_sending event..." | Re-trigger Greptile

sessionKeyForInternalHooks,
{
to,
content: payloadSummary.hookContent ?? payloadSummary.text,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Stale payload content sent to internal hook

The internal hook event is built from the outer payloadSummary variable (captured before applyMessageSendingHook ran), not from hookResult.payloadSummary. When a plugin hook modifies the message content (sendingResult.content != null), applyMessageSendingHook returns an updated payloadSummary in hookResult.payloadSummary — but that modified content is never used here. Internal hook subscribers will always see the pre-plugin-modification content, even when a plugin rewrote it.

Suggested change
content: payloadSummary.hookContent ?? payloadSummary.text,
content: hookResult.payloadSummary.hookContent ?? hookResult.payloadSummary.text,
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/infra/outbound/deliver.ts
Line: 1041

Comment:
**Stale payload content sent to internal hook**

The internal hook event is built from the outer `payloadSummary` variable (captured before `applyMessageSendingHook` ran), not from `hookResult.payloadSummary`. When a plugin hook modifies the message content (`sendingResult.content != null`), `applyMessageSendingHook` returns an updated `payloadSummary` in `hookResult.payloadSummary` — but that modified content is never used here. Internal hook subscribers will always see the pre-plugin-modification content, even when a plugin rewrote it.

```suggestion
                content: hookResult.payloadSummary.hookContent ?? hookResult.payloadSummary.text,
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +1032 to +1053
if (sessionKeyForInternalHooks) {
fireAndForgetHook(
triggerInternalHook(
createInternalHookEvent(
"message",
"sending",
sessionKeyForInternalHooks,
{
to,
content: payloadSummary.hookContent ?? payloadSummary.text,
channelId: channel,
accountId: accountId ?? undefined,
conversationId: to,
},
),
),
"deliverOutboundPayloads: message:sending internal hook failed",
(message) => {
log.warn(message);
},
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Internal hook fires for cancelled sends

The fireAndForgetHook call happens unconditionally before the if (hookResult.cancelled) { continue; } guard on line 1054. When a plugin hook sets cancel: true, the internal message_sending hook fires anyway — telling user-defined hooks that a message is being sent when it will actually never leave the system. Add a !hookResult.cancelled guard to the condition.

Suggested change
if (sessionKeyForInternalHooks) {
fireAndForgetHook(
triggerInternalHook(
createInternalHookEvent(
"message",
"sending",
sessionKeyForInternalHooks,
{
to,
content: payloadSummary.hookContent ?? payloadSummary.text,
channelId: channel,
accountId: accountId ?? undefined,
conversationId: to,
},
),
),
"deliverOutboundPayloads: message:sending internal hook failed",
(message) => {
log.warn(message);
},
);
}
if (sessionKeyForInternalHooks && !hookResult.cancelled) {
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/infra/outbound/deliver.ts
Line: 1032-1053

Comment:
**Internal hook fires for cancelled sends**

The `fireAndForgetHook` call happens unconditionally before the `if (hookResult.cancelled) { continue; }` guard on line 1054. When a plugin hook sets `cancel: true`, the internal `message_sending` hook fires anyway — telling user-defined hooks that a message is being sent when it will actually never leave the system. Add a `!hookResult.cancelled` guard to the condition.

```suggestion
      if (sessionKeyForInternalHooks && !hookResult.cancelled) {
```

How can I resolve this? If you propose a fix, please make it concise.

@clawsweeper

clawsweeper Bot commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs real behavior proof before merge.

Latest ClawSweeper review: 2026-05-20 02:11 UTC / May 19, 2026, 10:11 PM ET.

Workflow note: Future ClawSweeper reviews update this same comment in place.

How this review workflow works
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

Summary
The branch adds a fire-and-forget internal message:sending dispatch in outbound delivery after plugin message_sending hooks and rewrites outbound delivery tests to expect pre-send plus post-send internal hook calls.

Reproducibility: yes. at source level. Current main runs plugin message_sending before delivery, while directory hook events are registered verbatim and internal dispatch only matches message or message:sending keys.

PR rating
Overall: 🧂 unranked krab
Proof: 🧂 unranked krab
Patch quality: 🧂 unranked krab
Summary: Missing real behavior proof plus blocking hook-contract, event-ordering, payload-content, and test-drift issues make this PR not quality-ready yet.

Rank-up moves:

  • Add redacted real output from a standalone hook-directory setup showing the pre-send hook fires.
  • Fix the hook key, cancellation/blank-payload timing, and rewritten-content semantics with focused tests.
  • Restore unrelated outbound delivery test coverage and address the current failing checks.
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

Real behavior proof
Needs real behavior proof before merge: The PR body and comments provide rationale and CI status only; the contributor should add redacted terminal output, logs, or another artifact from a real hook-directory setup, then update the PR body or ask a maintainer to comment @clawsweeper re-review.

Risk before merge

  • Hooks registered exactly as message_sending can remain silent after merge because the patch emits only the colon-style internal event shape.
  • The new notification can fire for cancelled or non-deliverable payloads and can expose pre-rewrite content, which is unsafe for audit or guardrail subscribers.
  • The branch rewrites a large outbound delivery test surface and currently has failing checks, so green CI has not established the new behavior.
  • The contributor has not provided redacted real hook-directory output showing the standalone hook fires after the patch.

Maintainer options:

  1. Fix hook semantics before merge (recommended)
    Update the patch so the reported standalone hook key is reached, the event fires only for deliverable post-rewrite payloads, and unrelated test churn is restored.
  2. Pause for hook contract decision
    If maintainers want only message:sending, pause until the compatibility and documentation decision is explicit because the linked report uses message_sending.

Next step before merge
Needs contributor real behavior proof plus maintainer review of the standalone hook key and pre-send semantics before repair or merge.

Security
Needs attention: No dependency or CI supply-chain changes were found, but the hook-key mismatch and false/stale pre-send events affect guardrail-style hooks.

Review findings

  • [P1] Bridge the key standalone hooks register — src/infra/outbound/deliver.ts:1526-1527
  • [P2] Emit pre-send hooks only for deliverable payloads — src/infra/outbound/deliver.ts:1522
  • [P2] Send rewritten content to internal subscribers — src/infra/outbound/deliver.ts:1531
Review details

Best possible solution:

Define the standalone pre-send hook contract, add compatibility for the reported key or explicitly document the colon key, emit only after plugin rewrite/cancellation/effective-payload decisions are settled, restore focused tests, and prove it with a redacted hook-directory run.

Do we have a high-confidence way to reproduce the issue?

Yes, at source level. Current main runs plugin message_sending before delivery, while directory hook events are registered verbatim and internal dispatch only matches message or message:sending keys.

Is this the best way to solve the issue?

No. Shared outbound delivery is the right area, but this patch needs the hook-key contract, cancellation/blank-payload timing, rewritten-content semantics, and test drift fixed before it is the maintainable solution.

Label changes:

  • add status: 🛠️ actively grinding: The PR author has acted after the latest ClawSweeper review and work remains. Needs real behavior proof before merge: The PR body and comments provide rationale and CI status only; the contributor should add redacted terminal output, logs, or another artifact from a real hook-directory setup, then update the PR body or ask a maintainer to comment @clawsweeper re-review.
  • remove status: 📣 needs proof: Current PR status label is status: 🛠️ actively grinding.

Label justifications:

  • P2: This is a real hook-delivery fix with limited surface area, but it is not a core runtime outage.
  • merge-risk: 🚨 message-delivery: The patch can emit pre-send notifications for messages that are cancelled, blanked, or represented with stale content.
  • merge-risk: 🚨 security-boundary: Pre-send guardrail or audit hooks registered with the reported key can still be silently skipped after merge.
  • rating: 🧂 unranked krab: Current PR rating is 🧂 unranked krab because proof is 🧂 unranked krab, patch quality is 🧂 unranked krab, and Missing real behavior proof plus blocking hook-contract, event-ordering, payload-content, and test-drift issues make this PR not quality-ready yet.
  • status: 🛠️ actively grinding: The PR author has acted after the latest ClawSweeper review and work remains. Needs real behavior proof before merge: The PR body and comments provide rationale and CI status only; the contributor should add redacted terminal output, logs, or another artifact from a real hook-directory setup, then update the PR body or ask a maintainer to comment @clawsweeper re-review.

Full review comments:

  • [P1] Bridge the key standalone hooks register — src/infra/outbound/deliver.ts:1526-1527
    The linked report registers events: ["message_sending"], but this emits only createInternalHookEvent("message", "sending", ...). Since standalone events are registered verbatim and dispatch only matches message or message:sending, the reported hook can still remain silent.
    Confidence: 0.94
  • [P2] Emit pre-send hooks only for deliverable payloads — src/infra/outbound/deliver.ts:1522
    This block runs before hookResult.cancelled and before the later empty-payload skip. A plugin hook can cancel or blank a send, so the new internal hook can announce a message that will not be delivered.
    Confidence: 0.96
  • [P2] Send rewritten content to internal subscribers — src/infra/outbound/deliver.ts:1531
    The event uses the original payloadSummary, but applyMessageSendingHook returns updated content in hookResult.payloadSummary when a plugin rewrites outbound text. Internal subscribers would observe stale pre-rewrite content.
    Confidence: 0.96
  • [P1] Keep silent-reply suppression aligned with source — src/infra/outbound/deliver.test.ts:1630-1632
    The branch changes this case to expect a Matrix send for a bare NO_REPLY, but current payload planning drops silent entries before delivery and this PR does not change that source path. Keep the no-send assertion or make an intentional behavior change with matching docs and focused tests.
    Confidence: 0.9

Overall correctness: patch is incorrect
Overall confidence: 0.96

Security concerns:

  • [medium] Preserve pre-send guardrail compatibility — src/infra/outbound/deliver.ts:1526
    The PR emits message:sending while the reported standalone hook registers message_sending; because loader registration is verbatim and dispatch only matches type or colon keys, audit or guardrail hooks can appear loaded while still missing this path.
    Confidence: 0.9

Acceptance criteria:

  • node scripts/run-vitest.mjs src/infra/outbound/deliver.test.ts
  • node scripts/run-vitest.mjs src/hooks/internal-hooks.test.ts src/hooks/loader.test.ts

What I checked:

  • current_main_plugin_only_pre_send: Current main runs applyMessageSendingHook before delivery and only checks plugin message_sending hooks; there is no adjacent internal pre-send hook dispatch before cancellation/rendering/delivery. (src/infra/outbound/deliver.ts:1511, a002c416c7af)
  • internal_hook_key_contract: Internal hook dispatch only matches handlers registered for the general type or the colon-style type:action key, so message:sending does not reach a handler registered as message_sending. (src/hooks/internal-hooks.ts:268, a002c416c7af)
  • hook_loader_registers_events_verbatim: Directory hook metadata events are passed directly to registerInternalHook, preserving an underscore key from HOOK.md unless compatibility is added elsewhere. (src/hooks/loader.ts:171, a002c416c7af)
  • pr_head_added_block: The PR head adds the new internal hook block before the cancelled-send guard, emits message/sending, and builds content from the original payloadSummary. (src/infra/outbound/deliver.ts:1522, a0232d4e0e46)
  • rewrite_and_cancel_contract: applyMessageSendingHook returns cancelled: true for cancelled sends and returns updated payloadSummary when content is rewritten, which the PR block must respect. (src/infra/outbound/deliver.ts:1083, a002c416c7af)
  • silent_payload_plan_contract: Current main marks exact silent replies as silent and excludes them from the outbound payload plan; the PR changes a test to expect a send without changing this production path. (src/infra/outbound/payloads.ts:145, a002c416c7af)

Likely related people:

  • steipete: GitHub and local history show prior internal message hook bridge work and recent outbound delivery consolidation in the affected files. (role: recent outbound and internal hook bridge contributor; confidence: high; commits: f07bb8e8fc1c, a4b17d65a8ff, 5b59079fd4c2; files: src/infra/outbound/deliver.ts, src/hooks/internal-hooks.ts)
  • vincentkoc: Recent history touches hook loader behavior, internal hook contracts, and outbound delivery diagnostics adjacent to this pre-send hook path. (role: adjacent hook loader and outbound diagnostics contributor; confidence: medium; commits: 31de0335908b, bd32b1a906f3, 97d1b88e3faa; files: src/hooks/loader.ts, src/hooks/internal-hooks.ts, src/infra/outbound/deliver.ts)
  • Drickon: History attributes the global internal hook registry singleton and message hook context additions to this contributor, directly adjacent to registration visibility and dispatch key shape. (role: internal hook registry contributor; confidence: medium; commits: 0d8beeb4e5f5, b5102ba4f98e, e0b8b80067cf; files: src/hooks/internal-hooks.ts, src/hooks/internal-hooks.test.ts, src/hooks/message-hooks.test.ts)
  • Takhoffman: The PR rewrites silent-reply delivery expectations, and recent history links that policy surface to this contributor's silent reply prompt and routing work. (role: adjacent silent-reply policy contributor; confidence: low; commits: cc57d56b92f5; files: src/infra/outbound/deliver.test.ts, src/infra/outbound/payloads.ts)

Codex review notes: model gpt-5.5, reasoning high; reviewed against a002c416c7af.

@SymbolStar

Copy link
Copy Markdown
Contributor Author

Hi @steipete — friendly ping 🙏 CI is green now (rebased on latest main). Would appreciate a look when you get a chance!

@SymbolStar SymbolStar force-pushed the fix/73542-message-sending-internal-hook branch from 31f0096 to b8001cd Compare May 4, 2026 14:39
SymbolStar added 4 commits May 5, 2026 00:04
Fixes openclaw#73542

The message_sending event was only dispatched through the plugin hook
runner (hookRunner.runMessageSending) but never bridged to the internal
hook registry via triggerInternalHook. This meant user-defined hooks
registered for message_sending were never invoked.

Add triggerInternalHook(createInternalHookEvent("message", "sending", ...))
call mirroring the existing message_sent pattern.
The message:sending hook added in this PR calls createInternalHookEvent
and triggerInternalHook before delivery, so tests expecting only the
message:sent hook must now account for both calls.
Remove matrix/mattermost from publicPluginOwnedSdkEntrypoints since they
are already classified in supportedBundledFacadeSdkEntrypoints, causing
classification overlap failures.
@SymbolStar SymbolStar force-pushed the fix/73542-message-sending-internal-hook branch from b8001cd to 18e0269 Compare May 4, 2026 16:05
…ding-internal-hook

# Conflicts:
#	src/infra/outbound/deliver.test.ts
@openclaw-barnacle openclaw-barnacle Bot added size: XL triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. and removed size: S labels May 20, 2026
@SymbolStar

Copy link
Copy Markdown
Contributor Author

Merged latest main, resolved 1 conflict in src/infra/outbound/deliver.test.ts (kept HEAD assertions for the message:sending + message:sent hook pair — exactly what this PR adds; origin/main's single-hook expectation would defeat the fix). Head: a0232d4e0e469c716f5f5da526ae4d39c5def0e5. CI running.

@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. P2 Normal backlog priority with limited blast radius. merge-risk: 🚨 message-delivery 🚨 May drop, duplicate, misroute, suppress, or wrongly target messages. merge-risk: 🚨 security-boundary 🚨 May affect sandboxing, authorization, credentials, or sensitive data. labels May 20, 2026
@clawsweeper

clawsweeper Bot commented May 20, 2026

Copy link
Copy Markdown
Contributor

ClawSweeper PR egg

🎁 Pass real behavior proof to wake the egg and unlock a hatchable treat.

Where did the egg go?
  • The egg game starts only after the PR passes the real-behavior proof check.
  • Before that, no creature or rarity is rolled. The treat waits for real proof.
  • This is still just collectible flavor: proof affects review readiness, not creature quality.

…tics

The 'silentReply.direct' field was removed from SilentReplyPolicyShape
(direct is always disallowed by default). The expectation also needed
to flip from 'rewrite then send' to 'suppress send' to match the policy
that's actually implemented on main.
@SymbolStar

Copy link
Copy Markdown
Contributor Author

Updated deliver.test.ts silent-reply test to match current policy semantics on main:

  1. silentReply.direct no longer exists on SilentReplyPolicyShape (only group and internal are configurable; direct is the implicit default). Fixes the check-test-types TS2353.
  2. The renamed test ("applies silent-reply rewrite policy") was carrying a stale expectation. On main the corresponding test ("suppresses direct silent replies") expects sendMatrix to NOT be called. Reverted to the suppress-direct expectation. Fixes the checks-node-core-runtime-infra-state AssertionError.

New HEAD: 9391b1b.

@clawsweeper clawsweeper Bot added status: 🛠️ actively grinding The PR author has acted after the latest ClawSweeper review and work remains. and removed status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels May 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-risk: 🚨 message-delivery 🚨 May drop, duplicate, misroute, suppress, or wrongly target messages. merge-risk: 🚨 security-boundary 🚨 May affect sandboxing, authorization, credentials, or sensitive data. P2 Normal backlog priority with limited blast radius. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. size: XL status: 🛠️ actively grinding The PR author has acted after the latest ClawSweeper review and work remains. triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: message_sending hook event not firing for user-defined hooks

1 participant