Summary
Internal hooks registered for the message event type (specifically action: "sent") never fire, even when the hook is correctly registered and the delivery path does call triggerInternalHook("message", "sent", ...). The root cause is that registerInternalHook and triggerInternalHook operate on two separate handlers Map instances in the production bundle.
Environment
- OpenClaw version:
2026.2.25
- Channel: Feishu (DM)
- Hook metadata:
{ "events": ["message", "agent"] }
Steps to Reproduce
- Create a hook at
~/.openclaw/hooks/my-hook/handler.ts with metadata events: ["message", "agent"]
- Handle
event.type === "message" && event.action === "sent" in the handler
- Send a message through a Feishu session and trigger a reply from the agent
- Observe that the
message:sent handler branch is never reached (only message:received fires)
Add a console.log at the top of your handler and filter gateway.log — you will see type=message action=received events, but never type=message action=sent.
Root Cause Analysis
The internal hook system (handlers Map, registerInternalHook, triggerInternalHook) is defined in multiple bundle chunks:
| Chunk |
Role |
entry.js |
Main process; loadInternalHooks() calls registerInternalHook() here |
subsystem-Bqlcd6-a.js |
Imported by deliver-C3bAT7D8.js; triggerInternalHook() fires here |
Both files contain this line independently:
const handlers = /* @__PURE__ */ new Map();
Because each chunk instantiates its own handlers Map, hooks registered in entry.js's Map are invisible to triggerInternalHook in subsystem-Bqlcd6-a.js's Map — and vice versa.
Registration path:
gateway startup
→ loadInternalHooks() [entry.js]
→ registerInternalHook() [entry.js / Map A]
Trigger path:
agent reply
→ deliverOutboundPayloadsCore [deliver-C3bAT7D8.js]
→ triggerInternalHook() [subsystem-Bqlcd6-a.js / Map B] ← empty, no handlers registered
The same analysis applies to agent:end — no code path in the current bundle calls triggerInternalHook("agent", "end", ...) at all, so that event is effectively unimplemented.
Expected Behavior
message:sent fires in the registered hook handler every time the agent successfully delivers a reply
agent:end fires when the agent finishes processing a turn (useful for post-reply side effects such as adding a status reaction to the originating message)
Suggested Fix
Ensure registerInternalHook and triggerInternalHook share a single module-level singleton across all chunks. Two options:
- Preferred: Extract the
handlers Map into a dedicated singleton module (e.g., hook-registry.ts) and import from it everywhere — the bundler will then treat it as a single shared instance.
- Alternative: Use a process-level global (
globalThis.__ocHookHandlers) as the backing store, which survives chunk boundaries by design.
Workaround
Currently working around this by manually invoking a shell script after each reply:
~/.openclaw/hooks/feishu-status-reactions/add-done-reaction.sh
This is fragile and should not be necessary once the hook system works correctly.
Summary
Internal hooks registered for the
messageevent type (specificallyaction: "sent") never fire, even when the hook is correctly registered and the delivery path does calltriggerInternalHook("message", "sent", ...). The root cause is thatregisterInternalHookandtriggerInternalHookoperate on two separatehandlersMap instances in the production bundle.Environment
2026.2.25{ "events": ["message", "agent"] }Steps to Reproduce
~/.openclaw/hooks/my-hook/handler.tswith metadataevents: ["message", "agent"]event.type === "message" && event.action === "sent"in the handlermessage:senthandler branch is never reached (onlymessage:receivedfires)Add a
console.logat the top of your handler and filtergateway.log— you will seetype=message action=receivedevents, but nevertype=message action=sent.Root Cause Analysis
The internal hook system (
handlersMap,registerInternalHook,triggerInternalHook) is defined in multiple bundle chunks:entry.jsloadInternalHooks()callsregisterInternalHook()heresubsystem-Bqlcd6-a.jsdeliver-C3bAT7D8.js;triggerInternalHook()fires hereBoth files contain this line independently:
Because each chunk instantiates its own
handlersMap, hooks registered inentry.js's Map are invisible totriggerInternalHookinsubsystem-Bqlcd6-a.js's Map — and vice versa.Registration path:
Trigger path:
The same analysis applies to
agent:end— no code path in the current bundle callstriggerInternalHook("agent", "end", ...)at all, so that event is effectively unimplemented.Expected Behavior
message:sentfires in the registered hook handler every time the agent successfully delivers a replyagent:endfires when the agent finishes processing a turn (useful for post-reply side effects such as adding a status reaction to the originating message)Suggested Fix
Ensure
registerInternalHookandtriggerInternalHookshare a single module-level singleton across all chunks. Two options:handlersMap into a dedicated singleton module (e.g.,hook-registry.ts) and import from it everywhere — the bundler will then treat it as a single shared instance.globalThis.__ocHookHandlers) as the backing store, which survives chunk boundaries by design.Workaround
Currently working around this by manually invoking a shell script after each reply:
~/.openclaw/hooks/feishu-status-reactions/add-done-reaction.shThis is fragile and should not be necessary once the hook system works correctly.