openclaw - 💡(How to fix) Fix [Bug]: documented message-lifecycle hooks (message_sending / message_sent / agent_end) do not fire on Gateway HTTP /v1/chat/completions and /v1/responses

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

The documented public message-lifecycle hooks — message_sending, message_sent, agent_end, and (for the natural-answer revision case) before_agent_finalize — do not fire when the assistant replies through the Gateway's OpenAI-compatible HTTP endpoints (/v1/chat/completions, /v1/responses).

This is the same shape of gap as #70928 (Gateway chat.send for WebChat + openclaw-tui) and #52526 (CLI agent --json), extended to the REST surface. All three caller-visible result paths bypass the documented hook layer; only the persistence-side path (session-tool-result-guard's internal beforeMessageWriteHook) currently runs.

The Gateway HTTP API is documented as a primary integration surface for "Open WebUI, LobeChat, LibreChat, RAG pipelines, and agent-native clients" (docs/gateway/index.md). Plugins authoring hooks against the documented public Plugin SDK have no way to mutate or audit the assistant text those callers see.

Root Cause

The documented public message-lifecycle hooks — message_sending, message_sent, agent_end, and (for the natural-answer revision case) before_agent_finalize — do not fire when the assistant replies through the Gateway's OpenAI-compatible HTTP endpoints (/v1/chat/completions, /v1/responses).

This is the same shape of gap as #70928 (Gateway chat.send for WebChat + openclaw-tui) and #52526 (CLI agent --json), extended to the REST surface. All three caller-visible result paths bypass the documented hook layer; only the persistence-side path (session-tool-result-guard's internal beforeMessageWriteHook) currently runs.

The Gateway HTTP API is documented as a primary integration surface for "Open WebUI, LobeChat, LibreChat, RAG pipelines, and agent-native clients" (docs/gateway/index.md). Plugins authoring hooks against the documented public Plugin SDK have no way to mutate or audit the assistant text those callers see.

Fix Action

Fix / Workaround

The current workaround in techartdev's HA integration is a regex scrubber in conversation.py that strips tool_code fences post-arrival ([their PR #26][26]). That's a layer below where the documented contract should let the fix live.

Code Example

"gateway": {
     "bind": "lan",
     "http": { "endpoints": { "chatCompletions": { "enabled": true } } }
   }

---

api.on("message_sending", async (event) => {
     if (event.content?.includes("__MARKER__")) {
       return { content: "REWRITTEN" };
     }
     return { cancel: false };
   });

---

curl -s http://127.0.0.1:18789/v1/chat/completions \
     -H "Authorization: Bearer $TOKEN" \
     -H "Content-Type: application/json" \
     -d '{"model":"openclaw","messages":[{"role":"user","content":"emit __MARKER__ verbatim"}],"stream":false}' \
     | jq -r '.choices[0].message.content'

---

$ wc -l dist/openai-http-CjsV0iVU.js
508 dist/openai-http-CjsV0iVU.js

$ grep -nE "message_sending|message_sent|runMessageSending|runMessageSent|before_agent_reply|before_agent_finalize|runBeforeAgentReply|runBeforeAgentFinalize|agent_end|runAgentEnd" dist/openai-http-CjsV0iVU.js
# 0 matches

$ grep -n "/v1/chat/completions" dist/openai-http-CjsV0iVU.js
278:    pathname: "/v1/chat/completions",

$ grep -n "agentCommandFromIngress" dist/openai-http-CjsV0iVU.js
8:import { r as agentCommandFromIngress } from "./agent-command-DEmhTrQM.js";
353:    const result = await agentCommandFromIngress(commandInput, defaultRuntime, deps);
455:    const result = await agentCommandFromIngress(commandInput, defaultRuntime, deps);
RAW_BUFFERClick to expand / collapse

Repo: openclaw/openclaw Related: #70928 (Gateway chat.send — WebChat + openclaw-tui), #52526 (agent --json CLI), #50126 (umbrella: inconsistent hook coverage)


Summary

The documented public message-lifecycle hooks — message_sending, message_sent, agent_end, and (for the natural-answer revision case) before_agent_finalize — do not fire when the assistant replies through the Gateway's OpenAI-compatible HTTP endpoints (/v1/chat/completions, /v1/responses).

This is the same shape of gap as #70928 (Gateway chat.send for WebChat + openclaw-tui) and #52526 (CLI agent --json), extended to the REST surface. All three caller-visible result paths bypass the documented hook layer; only the persistence-side path (session-tool-result-guard's internal beforeMessageWriteHook) currently runs.

The Gateway HTTP API is documented as a primary integration surface for "Open WebUI, LobeChat, LibreChat, RAG pipelines, and agent-native clients" (docs/gateway/index.md). Plugins authoring hooks against the documented public Plugin SDK have no way to mutate or audit the assistant text those callers see.

Impact

Specifically, this breaks the message_sending contract — which the docs describe as "rewriting content or canceling delivery." A plugin registering api.on("message_sending", ...) to rewrite outbound assistant text:

  • works correctly on adapter-mediated channels (Telegram, Signal, etc.)
  • silently does nothing on /v1/chat/completions and /v1/responses

Concrete case: the techartdev/OpenClawHomeAssistantIntegration HACS plugin registers OpenClaw as a Home Assistant native conversation agent via OpenClaw's OpenAI-compat REST API. A workspace plugin attempting to translate model emissions (e.g. ```tool_code\nrecord_fact(...)\n``` markdown fences from gemma3 — which does not natively emit OpenAI-style tool_calls JSON) into clean confirmation text via message_sending would have no effect on the response payload HA's conversation integration receives. Voice PE TTS would speak the un-rewritten text verbatim.

The current workaround in techartdev's HA integration is a regex scrubber in conversation.py that strips tool_code fences post-arrival (their PR #26). That's a layer below where the documented contract should let the fix live.

Environment

  • OpenClaw: 2026.5.6 (c97b9f7)
  • Node: v24.15.0 (mise-managed)
  • OS: Fedora Server 44, kernel 6.19.10
  • Surface tested: POST /v1/chat/completions (gateway HTTP, gateway.http.endpoints.chatCompletions.enabled: true, --bind lan)
  • Caller: HA conversation.process via the techartdev HACS integration v0.1.62
  • Backend model: gemma3:27b-it-qat Q4_0 via local llama-server (Vulkan)

Reproduction

  1. Install OpenClaw 2026.5.6 (npm install -g [email protected]).

  2. Enable the OpenAI-compat REST endpoint in ~/.openclaw/openclaw.json:

    "gateway": {
      "bind": "lan",
      "http": { "endpoints": { "chatCompletions": { "enabled": true } } }
    }
  3. Register a workspace plugin that hooks message_sending to rewrite any assistant message containing a known marker (e.g. __MARKER__) to REWRITTEN:

    api.on("message_sending", async (event) => {
      if (event.content?.includes("__MARKER__")) {
        return { content: "REWRITTEN" };
      }
      return { cancel: false };
    });
  4. Send a chat-completions request that elicits the marker:

    curl -s http://127.0.0.1:18789/v1/chat/completions \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"model":"openclaw","messages":[{"role":"user","content":"emit __MARKER__ verbatim"}],"stream":false}' \
      | jq -r '.choices[0].message.content'

Actual

/v1/chat/completions choices[0].message.content returns __MARKER__. The hook never ran.

Expected

message_sending runs on the assistant message before the response payload is assembled, exactly as it does on adapter-mediated channels. choices[0].message.content returns REWRITTEN.

Source observations (current main, c97b9f7)

Empirical inspection of the installed dist confirms zero hook fires in the OpenAI-compat handler. The handler is in dist/openai-http-CjsV0iVU.js (508 lines):

$ wc -l dist/openai-http-CjsV0iVU.js
508 dist/openai-http-CjsV0iVU.js

$ grep -nE "message_sending|message_sent|runMessageSending|runMessageSent|before_agent_reply|before_agent_finalize|runBeforeAgentReply|runBeforeAgentFinalize|agent_end|runAgentEnd" dist/openai-http-CjsV0iVU.js
# 0 matches

$ grep -n "/v1/chat/completions" dist/openai-http-CjsV0iVU.js
278:    pathname: "/v1/chat/completions",

$ grep -n "agentCommandFromIngress" dist/openai-http-CjsV0iVU.js
8:import { r as agentCommandFromIngress } from "./agent-command-DEmhTrQM.js";
353:    const result = await agentCommandFromIngress(commandInput, defaultRuntime, deps);
455:    const result = await agentCommandFromIngress(commandInput, defaultRuntime, deps);

Both stream and non-stream paths call agentCommandFromIngress(...) and then serialize result.payloads directly into the OpenAI-compat response envelope. The agent-command bundle that handler imports (dist/agent-command-DEmhTrQM.js, 40 KB) likewise has zero matches for any of the message-lifecycle hook runner symbols.

This is the same agentCommandFromIngress seam clawsweeper identified in #52526's review for the CLI surface, and the same architectural pattern clawsweeper identified in #70928's review for chat.send ("Final chat broadcast has no sent hook emission … emitChatFinal(...) builds the final assistant payload and broadcasts it without runMessageSent or internal message:sent").

The result-builder upstream of the documented message-lifecycle hooks is shared across the CLI, Gateway chat.send, and Gateway HTTP REST surfaces. Each fix has so far been per-surface; the OpenAI-compat REST path is the third instance.

Suggested direction

Aligned with #52526 / #70928 / #50126:

  • Option A (per-surface fix, narrowly scoped): in the /v1/chat/completions and /v1/responses handlers in dist/openai-http-*.js, run the documented message-lifecycle hooks on the finalized assistant message before assembling the OpenAI-compat response envelope. Mirrors the fix shape clawsweeper proposed for #52526 (CLI) and #70928 (chat.send).
  • Option B (architectural): the unified OutboundDeliveryObserver direction described in #50126, where every caller-visible result path goes through one hook-firing seam. This addresses CLI, Gateway chat.send, OpenAI-compat REST, and any future caller-facing surfaces in one place.

No preference between the two — Option A matches what's been suggested for the other two surface fixes; Option B is the durable answer.

Notes

  • The before_message_write hook used internally in session-tool-result-guard.ts does fire on the persistence path (the .jsonl session transcript IS rewritten), but it's not part of the documented public Plugin SDK surface and shouldn't be relied on by plugin authors. The user-facing fix here is making the documented hooks (message_sending, etc.) work on this path.
  • The streaming variant (stream: true) is in scope: SSE chunks need to flow through the same finalization step or the same divergence appears in token-by-token TTS pipelines.
  • /v1/responses (the newer Responses API endpoint, listed alongside /v1/chat/completions in docs/gateway/index.md) presumably has the same gap; haven't tested it directly but the bundle structure suggests it shares the same handler family.
  • Happy to provide additional logs or open a PR targeting Option A for the OpenAI-compat REST path if a maintainer confirms direction.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - 💡(How to fix) Fix [Bug]: documented message-lifecycle hooks (message_sending / message_sent / agent_end) do not fire on Gateway HTTP /v1/chat/completions and /v1/responses