fix(outbound): thread sessionKey into message_sending + align session.key with agent runtime + document the contract#73706
Conversation
Greptile SummaryThis PR fixes a three-part bug where Confidence Score: 5/5This PR is safe to merge — changes are additive, well-tested, and confined to narrow plumbing points. No P0 or P1 issues found. The No files require special attention. Reviews (2): Last reviewed commit: "fix(outbound): align session.key with ag..." | Re-trigger Greptile |
|
Codex review: needs maintainer review before merge. Reviewed May 27, 2026, 1:10 PM ET / 17:10 UTC. Summary PR surface: Source +93, Tests +274, Other +193. Total +560 across 7 files. Reproducibility: yes. Source inspection shows current main has Review metrics: 2 noteworthy metrics.
Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Risk before merge
Maintainer options:
Next step before merge Security Review detailsBest possible solution: Land this PR after maintainer acceptance of the outbound hook session-key contract, and leave broader outbound hook coverage or Do we have a high-confidence way to reproduce the issue? Yes. Source inspection shows current main has Is this the best way to solve the issue? Yes, with maintainer acceptance of the contract. The code change is narrow, matches the existing mapper support, and the added tests/proof cover direct, no-session, and native redirect paths. AGENTS.md: found and applied where relevant. Codex review notes: model gpt-5.5, reasoning high; reviewed against 11dfef201f81. Label changesLabel changes:
Label justifications:
Evidence reviewedPR surface: Source +93, Tests +274, Other +193. Total +560 across 7 files. View PR surface stats
Acceptance criteria:
What I checked:
Likely related people:
What the crustacean ranks mean
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. How this review workflow works
|
The outbound delivery path in applyMessageSendingHook constructed a
PluginHookMessageContext containing only { channelId, accountId,
conversationId } and dropped the sessionKey that the surrounding
deliverOutboundPayloads scope already had as
sessionKeyForInternalHooks. Plugins receiving message_sending therefore
saw ctx.sessionKey === undefined for every reply.
This breaks any plugin that needs to correlate a per-turn signal
emitted in agent_end with the matching outbound delivery in
message_sending. The concrete observed regression is the
openclaw-provenance developer-mode taint footer: agent_end populates a
finalTaintBySession map keyed by sessionKey, and message_sending looks
it up by sessionKey before appending the footer. With the key missing
on the message_sending side, the lookup always returned undefined and
no footer was ever appended.
PluginHookMessageContext already declares sessionKey?: string and the
auto-reply dispatch path in src/auto-reply/dispatch.ts already
threads it through deriveInboundMessageHookContext +
toPluginMessageContext. This change brings deliver.ts in line.
- applyMessageSendingHook now takes an optional sessionKey and forwards
it on the runMessageSending context.
- The single caller in deliverOutboundPayloads passes
sessionKeyForInternalHooks, which is already resolved earlier in the
function from params.mirror?.sessionKey ?? params.session?.key.
- Two new unit tests cover the present-and-absent cases.
Closes the deliver.ts side of the agent_end → message_sending
correlation gap. No behavior change for plugins that don't read
ctx.sessionKey.
92b5c0b to
df71f5f
Compare
…ment contract
Builds on the previous commit. The original commit threaded the value
already in OutboundSessionContext.key into the message_sending hook
context. That was correct as a plumbing fix, but exposed a deeper
issue: dispatch-from-config.ts:488 was filling session.key with
ctx.SessionKey unconditionally, which is the right value for non-native
chat but the wrong value for native-command-redirect, where the agent
runtime ran against ctx.CommandTargetSessionKey.
The agent runner resolves its sessionKey as
`targetSessionKey || ctx.SessionKey` (see get-reply.ts:198). For
agent_end and message_sending to see the same canonical session key,
the dispatch path must mirror that resolution. We now do.
Changes
=======
src/auto-reply/reply/dispatch-from-config.ts
Compute agentRuntimeSessionKey using the same expression
get-reply.ts uses for the agent run, then pass it as
routeReply({ sessionKey }). For non-native chat (the common case)
this collapses to the existing ctx.SessionKey \u2014 no behavior change.
For native-command-redirect it now correctly follows the redirect
target so message_sending and agent_end agree.
src/infra/outbound/session-context.ts
Tighten the JSDoc on OutboundSessionContext.key + policyKey:
* key MUST equal the agent runtime params.sessionKey for the run
that produced the payload, naming the call sites that should
already be honoring this contract.
* policyKey is the delivery target's session for policy lookups
(silent-reply policy, send rate limits, agent-scoped channel
preferences) when delivery differs from the control session.
src/plugins/hook-message.types.ts
Tighten the JSDoc on PluginHookMessageContext.sessionKey to state
it is the same canonical key used by the agent runtime / agent_end /
llm_input / llm_output, so plugin authors correlating across hooks
know what to expect. Also document runId as the recommended per-turn
correlation field (UUID, stable across LLM iterations and retry
attempts within one end-to-end turn, distinct per inbound message
and per cron/heartbeat/followup run \u2014 more robust than sessionKey
for plugins that need to disambiguate concurrent turns in the same
session).
src/infra/outbound/deliver.ts
Document why sessionKeyForDeliveryDiagnostics falls back to
policyKey (best-effort identifier for telemetry only) and why
sessionKeyForInternalHooks deliberately does NOT fall back to
policyKey (handing the policy key to plugins that correlate against
agent_end would be wrong). Both blocks now have explicit comments.
src/infra/outbound/deliver.test.ts
Add a contract test asserting that when session.key and
session.policyKey differ, message_sending receives session.key
(never policyKey). Brings deliver.test.ts to 56/56 passing
(53 prior + 3 new).
No semantic change for the regular Discord/Slack chat path. The fix
is meaningful for native-command-redirect flows; the JSDoc/comment
updates are the larger value of the change \u2014 the contract was
implicit and only honored by some callers, and downstream plugins
that correlate hooks need a documented invariant they can rely on.
|
@greptile-apps: We have added some documentation to fully explain the hook surface contract regarding session key vs policy key. please re-review. |
The developer-mode taint footer was being silently dropped on outbound
replies. Root cause: the per-session correlation maps used by the
agent_end \u2192 message_sending hook chain were declared inside
`registerSecurityHooks`, which is invoked once per agent context
(tank, narcissus, shiva, smith, main, ...). Each invocation produced
its own closure with its own Map. agent_end SET in one closure's Map
landed invisibly to message_sending GET in another closure's Map for
the same outbound delivery, so `finalTaintBySession.get(sessionKey)`
always returned undefined and the footer was never appended.
Empirical evidence (gateway log, 2026-04-28):
agent_end SET fullKey=agent:tank:discord:tank:direct:1594 mapInstance=d3j2d7fe
message_sending sessionKey=agent:tank:discord:tank:direct:1594 lookupHit=false mapInstance=amk32ibv
message_sending sessionKey=agent:tank:discord:tank:direct:1594 lookupHit=true mapInstance=d3j2d7fe
5 distinct mapInstance ids = 5 plugin closures from 5 agent contexts,
all alive within a single module load. After the fix:
moduleLoadCount=1
agent_end FIRED setting fullKey=agent:tank:discord:tank:direct:1594 mapSize_before=0
message_sending FIRED mapSize=1 mapKeys=["agent:tank:discord:tank:direct:1594"]
\u2192 footer rendered \u2705
Fix:
* Promote four per-session maps from function scope to a single
process-wide state object anchored on globalThis via
`Symbol.for("openclaw.provenance.processState.v1")`. Module scope
alone would already be sufficient at `moduleLoadCount=1`, but
using a globalThis-keyed Symbol is defense-in-depth: even if the
plugin module were ever re-evaluated (CommonJS/ESM dual instances,
agent-scoped node_modules resolution, etc.), all loads would
converge on the same Maps.
* Maps now shared:
- finalTaintBySession agent_end \u2192 message_sending
- turnStartTaintBySession before_prompt_build / inbound_claim \u2192 agent_end
- lastImpactedToolBySession after_tool_call \u2192 agent_end
- blockedToolsBySession before_tool_call \u2192 agent_end clear
* Maps that stay function-scoped (single-instance read+write only):
turnStartTimes, lastLlmNodeBySession, sessionAgentMap.
The full chain also depended on a separate core fix (openclaw/openclaw#73706)
that threads sessionKey into the message_sending hook context via
deliver.ts. Either fix alone leaves the footer broken; together they
restore developer-mode footers on every outbound reply, including the
first one after a fresh gateway boot.
The developer-mode taint footer was being silently dropped on outbound
replies. Root cause: the per-session correlation maps used by the
agent_end \u2192 message_sending hook chain were declared inside
`registerSecurityHooks`, which is invoked once per agent context
(tank, narcissus, shiva, smith, main, ...). Each invocation produced
its own closure with its own Map. agent_end SET in one closure's Map
landed invisibly to message_sending GET in another closure's Map for
the same outbound delivery, so `finalTaintBySession.get(sessionKey)`
always returned undefined and the footer was never appended.
Empirical evidence (gateway log, 2026-04-28):
agent_end SET fullKey=agent:tank:discord:tank:direct:1594 mapInstance=d3j2d7fe
message_sending sessionKey=agent:tank:discord:tank:direct:1594 lookupHit=false mapInstance=amk32ibv
message_sending sessionKey=agent:tank:discord:tank:direct:1594 lookupHit=true mapInstance=d3j2d7fe
5 distinct mapInstance ids = 5 plugin closures from 5 agent contexts,
all alive within a single module load. After the fix:
moduleLoadCount=1
agent_end FIRED setting fullKey=agent:tank:discord:tank:direct:1594 mapSize_before=0
message_sending FIRED mapSize=1 mapKeys=["agent:tank:discord:tank:direct:1594"]
\u2192 footer rendered \u2705
Fix:
* Promote four per-session maps from function scope to a single
process-wide state object anchored on globalThis via
`Symbol.for("openclaw.provenance.processState.v1")`. Module scope
alone would already be sufficient at `moduleLoadCount=1`, but
using a globalThis-keyed Symbol is defense-in-depth: even if the
plugin module were ever re-evaluated (CommonJS/ESM dual instances,
agent-scoped node_modules resolution, etc.), all loads would
converge on the same Maps.
* Maps now shared:
- finalTaintBySession agent_end \u2192 message_sending
- turnStartTaintBySession before_prompt_build / inbound_claim \u2192 agent_end
- lastImpactedToolBySession after_tool_call \u2192 agent_end
- blockedToolsBySession before_tool_call \u2192 agent_end clear
* Maps that stay function-scoped (single-instance read+write only):
turnStartTimes, lastLlmNodeBySession, sessionAgentMap.
The full chain also depended on a separate core fix (openclaw/openclaw#73706)
that threads sessionKey into the message_sending hook context via
deliver.ts. Either fix alone leaves the footer broken; together they
restore developer-mode footers on every outbound reply, including the
first one after a fresh gateway boot.
27516b5 to
b4ecfe2
Compare
…(review feedback) What ---- Address all three findings from Clawsweeper's review of openclaw#73706, plus attach the after-fix real-runtime behavior proof maintainer review asked for. - [P2] Align hook docs with delivered outbound fields: Thread the canonical outbound `sessionKey` into the `message_sent` plugin hook context as well, so plugins observing both `message_sending` and `message_sent` see the same `sessionKey` (and so it matches the value the internal `message:sent` hook already fires with). The value is already computed for the internal hook in `deliverOutboundPayloadsCore`; we just reuse it in `createMessageSentEmitter`. JSDoc on `PluginHookMessageContext` is narrowed to honestly describe what is actually plumbed: `sessionKey` flows into both outbound delivery hooks when delivery has a session attached, and `runId` is currently NOT plumbed through outbound delivery (only inbound + agent-runtime hooks), so plugins must use `sessionKey` for `agent_end` <-> `message_sending` correlation today. - [P3] Add the hook fix to the changelog: Single Plugins/hooks entry under Unreleased that calls out the outbound `sessionKey` threading and the native-redirect alignment. - [P3] Cover native redirect session-key selection: Two focused regression tests in `dispatch-from-config.test.ts` pin the routed reply session-key contract: native redirect with `CommandTargetSessionKey` set must route via the redirect-target session, while non-native (text) commands must keep the inbound `SessionKey` even if `CommandTargetSessionKey` happens to be populated. These guard against future divergence between `agent_end` and `message_sending` on native redirects. After-fix real behavior proof ----------------------------- Added `scripts/proof-73706-message-sending-session-key.ts`, a self-checking real-runtime harness that wires up the production `deliverOutboundPayloads` path against a real `PluginRegistry`, real `getGlobalHookRunner`/`initializeGlobalHookRunner` singleton, and a real channel plugin with `sendText`. It registers actual `message_sending` / `message_sent` hook handlers, exercises three scenarios (with session, without session, native redirect target), and asserts the captured runtime ctx values match the documented contract. Catches the `message_sent` regression I had introduced locally (the unit test mock was happy because vitest mocked the hook runner; the real runtime was not). Output captured under `~/reports/proof-73706/run-output.txt` and pasted into the PR comment. Validation gate --------------- - pnpm tsgo:core: clean - pnpm tsgo:core:test: clean - pnpm vitest run src/infra/outbound/deliver.test.ts: 64/64 passed - pnpm vitest run src/auto-reply/reply/dispatch-from-config.test.ts: 105/105 passed - pnpm vitest run src/hooks/message-hook-mappers.test.ts: 11/11 passed - pnpm oxlint <touched files>: 0 warnings, 0 errors - pnpm plugin-sdk:api:check: OK (regenerated baseline hash for the PluginHookMessageContext JSDoc edit; only the .sha256 changes) - git diff --check: clean - pnpm tsx scripts/proof-73706-message-sending-session-key.ts: all three runtime assertions pass Beads: openclaw-t9l
Round 4 — addressing Clawsweeper review feedbackRound-4 commit: Each finding from the previous review is addressed individually below, followed by the after-fix real-runtime behavior proof maintainer review asked for. [P2] Align hook docs with delivered outbound fields —
|
|
Reviewed the latest ClawSweeper comment and prepared the requested |
|
ClawSweeper changelog finding addressed.
@clawsweeper please re-review. |
|
Landing verification for PR #73706. Behavior addressed: outbound plugin hooks now receive the canonical delivery sessionKey for message_sending and message_sent; native command redirects route outbound delivery with the same session key used by the agent runtime. Exact PR/head reviewed: 1a0af36 Exact checks reviewed before merge:
Evidence after fix:
What was not tested locally in this landing step: I did not rerun local pnpm tests because the PR already had green current-head CI plus the dedicated real behavior proof check. Thanks @zeroaltitude. |
|
Landed in 05db911.
Thanks @zeroaltitude. |
Thread the canonical outbound session key into plugin message_sending and message_sent hook contexts, and align native command redirect routed delivery with the agent runtime session key. This lets plugins correlate agent_end with outbound delivery hooks without seeing missing or divergent session keys. Verification: - gh pr checks 73706 --repo openclaw/openclaw --watch=false - Real behavior proof: https://github.com/openclaw/openclaw/actions/runs/26526635074/job/78131933497 Thanks @zeroaltitude. Co-authored-by: Edward Abrams <[email protected]>
…026.5.28) (#759) This PR contains the following updates: | Package | Update | Change | |---|---|---| | [ghcr.io/openclaw/openclaw](https://openclaw.ai) ([source](https://github.com/openclaw/openclaw)) | patch | `2026.5.27` → `2026.5.28` | --- ### Release Notes <details> <summary>openclaw/openclaw (ghcr.io/openclaw/openclaw)</summary> ### [`v2026.5.28`](https://github.com/openclaw/openclaw/blob/HEAD/CHANGELOG.md#2026528) [Compare Source](openclaw/openclaw@v2026.5.27...v2026.5.28) ##### Highlights - Agent and Codex runtime recovery is steadier: subagents keep cwd/workspace separation, hook context stays prompt-local, session locks release on timeout abort while live OpenClaw locks survive cleanup, stale restart continuations are avoided, and Codex app-server/helper failures no longer tear down shared runtime state. ([#​87218](openclaw/openclaw#87218), [#​86875](openclaw/openclaw#86875), [#​87409](openclaw/openclaw#87409), [#​87399](openclaw/openclaw#87399), [#​87375](openclaw/openclaw#87375), [#​88129](openclaw/openclaw#88129)) - Channel delivery and session identity got safer across outbound plugin hooks, Matrix room ids, iMessage reactions/approvals, Slack final replies, Discord recovered tool warnings, runtime-config message actions, WhatsApp profile auth roots, Telegram polling, and Microsoft Teams service URL trust checks. ([#​73706](openclaw/openclaw#73706), [#​75670](openclaw/openclaw#75670), [#​87366](openclaw/openclaw#87366), [#​87451](openclaw/openclaw#87451), [#​87334](openclaw/openclaw#87334), [#​84535](openclaw/openclaw#84535), [#​82492](openclaw/openclaw#82492), [#​83304](openclaw/openclaw#83304), [#​87160](openclaw/openclaw#87160)) - Mobile and chat surfaces got a broader refresh: the iOS Pro UI, hosted push relay default, realtime Talk tab playback, Gateway chat transport, onboarding, Talk permissions, WebChat reconnect delivery, and session picker behavior now preserve more state across reconnects and empty searches. ([#​87367](openclaw/openclaw#87367), [#​87531](openclaw/openclaw#87531), [#​87682](openclaw/openclaw#87682), [#​88096](openclaw/openclaw#88096), [#​88105](openclaw/openclaw#88105)) Thanks [@​ngutman](https://github.com/ngutman) and [@​BunsDev](https://github.com/BunsDev). - Browser, channel, and automation inputs are stricter: Browser tool timeouts, viewport/tab indices, Gateway ports, cron retry handling, Discord component ids, schema array refs, Telegram callback pages, and channel progress callbacks now reject malformed values earlier and preserve the intended delivery context. ([#​82887](openclaw/openclaw#82887)) - Provider, media, and document coverage expands with Claude Opus 4.8, Fal Krea image schemas, NVIDIA featured models, MiniMax streaming music responses, encrypted PDF extraction, voice model catalogs, GitHub Copilot agent runtime support, and a Codex Supervisor plugin path for delegated Codex workflows. ([#​87845](openclaw/openclaw#87845), [#​87890](openclaw/openclaw#87890), [#​80775](openclaw/openclaw#80775), [#​84764](openclaw/openclaw#84764), [#​87751](openclaw/openclaw#87751), [#​87794](openclaw/openclaw#87794)) - CLI, auth, doctor, and provider paths fail faster and recover more clearly: malformed numeric/version options are rejected, workspace dotenv provider credentials are ignored, heartbeat defaults, OAuth/token lifetimes, and local service startup requests are bounded, agent auth health labels are clearer, legacy `api_key` auth profiles migrate to canonical form, and restart guidance is actionable. ([#​87398](openclaw/openclaw#87398), [#​86281](openclaw/openclaw#86281), [#​87361](openclaw/openclaw#87361), [#​88133](openclaw/openclaw#88133), [#​83655](openclaw/openclaw#83655), [#​87559](openclaw/openclaw#87559), [#​88088](openclaw/openclaw#88088), [#​85924](openclaw/openclaw#85924)) Thanks [@​vincentkoc](https://github.com/vincentkoc) and [@​giodl73-repo](https://github.com/giodl73-repo). - Plugin and Gateway hot paths do less repeated work while preserving cache correctness for install records, config JSON parsing, tool search catalogs, session stores, manifest model rows, auto-enabled plugin config, browser tokens, viewer assets, and release-split external plugin packages. ([#​86699](openclaw/openclaw#86699)) - Release, QA, and E2E validation now bound more log, artifact, harness, and cross-OS waits so failing lanes produce proof instead of hanging or false-greening. ##### Changes - Status: show active subagent details in status output. - Diffs: split the default language pack and expand default Diffs language coverage while keeping the host floor aligned. ([#​87370](openclaw/openclaw#87370), [#​87372](openclaw/openclaw#87372)) Thanks [@​RomneyDa](https://github.com/RomneyDa). - ClawHub: add plugin display names plus skill verification and trust surfaces. ([#​87354](openclaw/openclaw#87354), [#​86699](openclaw/openclaw#86699)) Thanks [@​thewilloftheshadow](https://github.com/thewilloftheshadow) and [@​Patrick-Erichsen](https://github.com/Patrick-Erichsen). - iOS: refresh the dev app with Pro Command, Chat, Agents, Settings, hosted push relay defaults, and realtime Talk playback wired to gateway sessions, diagnostics, chat, and realtime Talk. ([#​87367](openclaw/openclaw#87367), [#​88096](openclaw/openclaw#88096), [#​88105](openclaw/openclaw#88105)) Thanks [@​Solvely-Colin](https://github.com/Solvely-Colin) and [@​ngutman](https://github.com/ngutman). - Docs: clarify Codex computer-use setup, paste-token stdin auth setup, macOS gateway sleep troubleshooting, native Codex hook relay recovery, container model auth, install deployment cards, device-token admin gating, CLI setup flow compatibility, Notte cloud browser CDP setup, and backport targets. ([#​87313](openclaw/openclaw#87313), [#​63050](openclaw/openclaw#63050), [#​87685](openclaw/openclaw#87685)) Thanks [@​bdjben](https://github.com/bdjben), [@​liaoandi](https://github.com/liaoandi), and [@​thewilloftheshadow](https://github.com/thewilloftheshadow). - PDF/tools: use ClawPDF for PDF extraction, support encrypted PDF extraction, and surface MCP structured content in agent tool results. ([#​87670](openclaw/openclaw#87670), [#​87751](openclaw/openclaw#87751)) - Providers: add Claude Opus 4.8 support, Fal Krea image model schemas, NVIDIA featured model catalogs, MiniMax streaming music responses, and provider-backed voice model catalogs. ([#​87845](openclaw/openclaw#87845), [#​87890](openclaw/openclaw#87890), [#​80775](openclaw/openclaw#80775), [#​84764](openclaw/openclaw#84764), [#​87794](openclaw/openclaw#87794)) Thanks [@​eleqtrizit](https://github.com/eleqtrizit) and [@​vincentkoc](https://github.com/vincentkoc). - Codex/GitHub: add the GitHub Copilot agent runtime and the Codex Supervisor plugin package. - Plugins: externalize GitHub Copilot and Tokenjuice as official install-on-demand plugins with npm and ClawHub publish metadata. - Workboard: add agent coordination tools for tracking and handing off active agent work. - Discord: show commentary in progress drafts so live Discord runs expose useful in-progress context. ([#​85200](openclaw/openclaw#85200)) - Plugin SDK: add a reply payload sending hook for plugins that need to deliver channel-owned replies and flatten package types for SDK declarations. ([#​82823](openclaw/openclaw#82823), [#​87165](openclaw/openclaw#87165)) Thanks [@​piersonr](https://github.com/piersonr) and [@​RomneyDa](https://github.com/RomneyDa). - Policy: add policy comparison, ingress-channel conformance, and sandbox-posture conformance checks. ([#​85572](openclaw/openclaw#85572), [#​85744](openclaw/openclaw#85744), [#​86768](openclaw/openclaw#86768)) ##### Fixes - Agents: fall back to local config pruning when the optional `agents delete` Gateway probe cannot authenticate, so offline installs can still delete agents without removing shared workspaces. - Tighten phone-control mutation authorization \[AI]. ([#​87150](openclaw/openclaw#87150)) Thanks [@​pgondhi987](https://github.com/pgondhi987). - Clarify directive persistence authorization policy \[AI]. ([#​86369](openclaw/openclaw#86369)) Thanks [@​pgondhi987](https://github.com/pgondhi987). - Agents/Codex: keep spawned agent cwd/workspace state separated, forward ACP spawn attachments, keep hook context prompt-local, release session locks on timeout abort and runtime teardown without deleting live OpenClaw-owned locks during cleanup, avoid session event queue self-wait, clean up exec abort listeners, stream assistant deltas incrementally, recover raw missing-thread compaction failures, preserve rotated compaction session identity, keep compaction-timeout snapshots continuable, preserve shared app-server state across startup or helper failures, keep native hook relay alive across restarts and prune stale bridge files, close native hook relay replacement races, keep Claude live tool progress visible for watchdog recovery, suppress abandoned requester completion handoff, route workspace memory through tools, resolve Codex runtime models first, report quarantined dynamic tools, format `skills` command output, bind node auto-review to prepared plans, retry Claude CLI transcript probes, and bound compaction/steering retries. ([#​87218](openclaw/openclaw#87218), [#​86875](openclaw/openclaw#86875), [#​86123](openclaw/openclaw#86123), [#​88129](openclaw/openclaw#88129), [#​87399](openclaw/openclaw#87399), [#​87375](openclaw/openclaw#87375), [#​72574](openclaw/openclaw#72574), [#​87383](openclaw/openclaw#87383), [#​87400](openclaw/openclaw#87400), [#​83022](openclaw/openclaw#83022), [#​87671](openclaw/openclaw#87671), [#​87738](openclaw/openclaw#87738), [#​87747](openclaw/openclaw#87747), [#​87706](openclaw/openclaw#87706), [#​87546](openclaw/openclaw#87546), [#​87541](openclaw/openclaw#87541), [#​81048](openclaw/openclaw#81048)) Thanks [@​mbelinky](https://github.com/mbelinky), [@​Alix-007](https://github.com/Alix-007), [@​luoyanglang](https://github.com/luoyanglang), [@​yetval](https://github.com/yetval), [@​sjf](https://github.com/sjf), [@​joshavant](https://github.com/joshavant), [@​benjamin1492](https://github.com/benjamin1492), [@​c19354837](https://github.com/c19354837), [@​fuller-stack-dev](https://github.com/fuller-stack-dev), [@​pfrederiksen](https://github.com/pfrederiksen), and [@​dodge1218](https://github.com/dodge1218). - Codex Supervisor: keep real-home app-server MCP session listing on the loaded state path, bound stored history scans, and close WebSocket probes cleanly. - Channels: thread canonical session keys into outbound hooks, preserve Matrix room-id case, keep fallback tool warnings mention-inert, retain delivered Slack final replies during late cleanup, continue iMessage polling after denied reactions, suppress duplicate native exec approvals, resolve Gateway message actions against the active runtime config, preserve Telegram SecretRef prompt config and polling keepalives, preserve WhatsApp profile auth roots, QR display, document filenames, and plugin hook config, suppress Discord recovered tool warnings, preserve the Discord voice outbound helper, cap Discord/Signal/Zalo channel request and container timeouts, and block untrusted Teams service URLs while keeping TeamsSDK patterns aligned. ([#​73706](openclaw/openclaw#73706), [#​75670](openclaw/openclaw#75670), [#​87366](openclaw/openclaw#87366), [#​87451](openclaw/openclaw#87451), [#​87465](openclaw/openclaw#87465), [#​87334](openclaw/openclaw#87334), [#​84535](openclaw/openclaw#84535), [#​76262](openclaw/openclaw#76262), [#​83304](openclaw/openclaw#83304), [#​82492](openclaw/openclaw#82492), [#​87581](openclaw/openclaw#87581), [#​77114](openclaw/openclaw#77114), [#​86426](openclaw/openclaw#86426), [#​85529](openclaw/openclaw#85529), [#​87160](openclaw/openclaw#87160)) Thanks [@​zeroaltitude](https://github.com/zeroaltitude), [@​lukeboyett](https://github.com/lukeboyett), [@​jarvis-mns1](https://github.com/jarvis-mns1), [@​xiaotian](https://github.com/xiaotian), [@​funmerlin](https://github.com/funmerlin), [@​joshavant](https://github.com/joshavant), [@​eleqtrizit](https://github.com/eleqtrizit), [@​heyitsaamir](https://github.com/heyitsaamir), [@​amittell](https://github.com/amittell), [@​lidge-jun](https://github.com/lidge-jun), [@​liorb-mountapps](https://github.com/liorb-mountapps), [@​masatohoshino](https://github.com/masatohoshino), [@​bladin](https://github.com/bladin), and [@​giodl73-repo](https://github.com/giodl73-repo). - CLI/auth/doctor/providers: reject malformed numeric/timeout/subcommand-version inputs, ignore workspace dotenv provider credentials, wait for respawn child shutdown, bound heartbeat defaults plus Codex, GitHub Copilot, OpenAI, Anthropic, Google, Feishu, LM Studio, MiniMax, Xiaomi TTS, and local-provider OAuth/token/model requests, harden Codex auth probes, label auth health by agent, preserve explicit agentRuntime pins during Codex model migration, warm provider auth off the main thread, honor Codex response timeouts, stop migrating current Claude Haiku 4.5 profiles to Sonnet, bound local service startup, resolve GPT-5.5 without cached catalog, migrate legacy memory auto-provider config, rewrite non-canonical `api_key` auth profiles, and make doctor restart follow-ups actionable. ([#​87398](openclaw/openclaw#87398), [#​86281](openclaw/openclaw#86281), [#​87361](openclaw/openclaw#87361), [#​88133](openclaw/openclaw#88133), [#​83655](openclaw/openclaw#83655), [#​87559](openclaw/openclaw#87559), [#​87719](openclaw/openclaw#87719), [#​88088](openclaw/openclaw#88088), [#​85924](openclaw/openclaw#85924), [#​84362](openclaw/openclaw#84362)) Thanks [@​Patrick-Erichsen](https://github.com/Patrick-Erichsen), [@​samzong](https://github.com/samzong), [@​giodl73-repo](https://github.com/giodl73-repo), [@​alkor2000](https://github.com/alkor2000), [@​mmaps](https://github.com/mmaps), [@​nxmxbbd](https://github.com/nxmxbbd), and [@​vincentkoc](https://github.com/vincentkoc). - Gateway/security/session state: expire browser tokens after auth rotation, scope assistant idempotency dedupe, drain probe client closes, avoid stale restart continuation reuse, preserve retry-after fallbacks and stale rate-limit cooldown probes, bound webchat image and artifact transcript scans, include seconds in inbound metadata timestamps, clear completed session active runs, clear stale chat stream buffers, and evict current plugin-state namespaces at row caps. ([#​87810](openclaw/openclaw#87810), [#​87833](openclaw/openclaw#87833), [#​75089](openclaw/openclaw#75089)) Thanks [@​joshavant](https://github.com/joshavant) and [@​litang9](https://github.com/litang9). - Config/parsing/network: reject partial numeric parsing, parse provider/Discord retry headers and dates strictly, honor IPv6 and bare IPv6 `no_proxy` entries, preserve empty plugin allowlists, canonicalize secret target array indexes, and reject malformed media content lengths, inspected TCP ports, marketplace content lengths, cron epochs, sandbox stat fields, unsafe duration values, empty config path segments, noncanonical schema array refs, unsafe Telegram callback pages, and invalid Teams attachment-fetch DNS targets. ([#​87883](openclaw/openclaw#87883)) Thanks [@​zhangguiping-xydt](https://github.com/zhangguiping-xydt). - Browser/input hardening: reject invalid tab indexes, excessive viewport resizes, explicit zero CDP ports, malformed geolocation options, unsafe screenshot or permission-grant timeouts, loose response-body limits, invalid cookie expiries, and non-finite Browser tool delays/timeouts. - Cron/automation: retry recurring jobs after transient model rate limits before waiting for the next scheduled slot, and preflight model fallbacks before skipping scheduled work. ([#​82887](openclaw/openclaw#82887)) Thanks [@​chen-zhang-cs-code](https://github.com/chen-zhang-cs-code). - Auto-reply/directives: respect provider and relayed channel metadata during directive persistence so channel-originated decisions keep their intended context. ([#​87683](openclaw/openclaw#87683)) - WhatsApp: resolve the auth directory from the active profile so profile-scoped WhatsApp installs do not drift to the wrong credential root. ([#​82492](openclaw/openclaw#82492)) Thanks [@​lidge-jun](https://github.com/lidge-jun). - Gateway/session state: clear completed session active runs, avoid cold-loading providers for MCP inventory, cache single-session child indexes, cap handshake timers, and bound preauth, auth-guard, media, transcript, readiness, and port options. - Channels/replies: preserve channel-owned progress callbacks when verbose output is off, keep group-room progress suppression intact, prefer external session delivery context, escape Discord component id delimiters, force final TUI chat repaints, show Slack reasoning previews, and normalize Discord/Matrix/Mattermost channel numeric options. ([#​87476](openclaw/openclaw#87476), [#​87423](openclaw/openclaw#87423)) - Agents/tool args: harden smart-quoted argument repair for edit arrays and exact escaped arguments so model-produced tool calls recover without corrupting valid input. ([#​86611](openclaw/openclaw#86611)) Thanks [@​ferminquant](https://github.com/ferminquant). - Providers/agents: preserve seeded Anthropic signatures, preserve signed thinking payloads, concatenate signature-delta chunks, preserve DeepSeek `reasoning_content` replay across tier suffixes, apply OpenRouter strict9 ids to Mistral routes, promote Ollama plain-text tool calls, load NVIDIA featured model catalogs, stream MiniMax music generation responses, and recover empty preflight compaction. ([#​87593](openclaw/openclaw#87593), [#​87493](openclaw/openclaw#87493), [#​80775](openclaw/openclaw#80775), [#​84764](openclaw/openclaw#84764)) Thanks [@​Pluviobyte](https://github.com/Pluviobyte) and [@​eleqtrizit](https://github.com/eleqtrizit). - Media/images: skip CLI image cache refs when resolving generated images, allow trusted generated HTML attachments, and bound generated video downloads so stale refs and slow providers fail cleanly. ([#​87523](openclaw/openclaw#87523), [#​87982](openclaw/openclaw#87982)) - File transfer: handle late tar stdin pipe errors after archive validation or unpacking has already settled. - Performance: trust install-record caches between reloads, prefer native JSON parsing, reuse unchanged tool-search catalogs, reuse gateway session and plugin metadata paths, skip unchanged store serialization, patch single-entry session writes, add precomputed session patch writers, reduce store clone allocations, cache manifest model catalog rows and auto-enabled plugin config, avoid full session snapshots for entry reads, defer configured Slack full startup, prefer bundled plugin dist entries, and slim current metadata identity caches. ([#​87760](openclaw/openclaw#87760)) - Docker/release/QA: package runtime workspace templates, stream cross-OS served artifacts, preserve sparse Crabbox run artifacts, isolate npm plugin installs per package, reject incompatible package plugin API installs, drop the leftover root Sharp dependency from package manifests after the Rastermill migration, bound OpenClaw instance logs, plugin gauntlet relay logs, MCP channel buffers, kitchen-sink scans, agent-turn assertions, QA-Lab credential broker calls, QA Matrix substrate requests, and release scenario logs, and keep release/google live guards current. ([#​87647](openclaw/openclaw#87647), [#​87477](openclaw/openclaw#87477)) Thanks [@​rohitjavvadi](https://github.com/rohitjavvadi) and [@​vincentkoc](https://github.com/vincentkoc). - Release/CI: bound manual git fetches, ClawHub verifier responses, ClawHub owner metadata, dependency-guard error bodies, Parallels limits, startup/test/memory budget parsing, and diffs viewer build warnings so release lanes fail with useful proof instead of hanging. ([#​87839](openclaw/openclaw#87839)) </details> --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about these updates again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xMDEuMSIsInVwZGF0ZWRJblZlciI6IjQzLjEwMS4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJyZW5vdmF0ZS9jb250YWluZXIiLCJ0eXBlL3BhdGNoIl19--> Reviewed-on: https://git.erwanleboucher.dev/eleboucher/homelab/pulls/759
Thread the canonical outbound session key into plugin message_sending and message_sent hook contexts, and align native command redirect routed delivery with the agent runtime session key. This lets plugins correlate agent_end with outbound delivery hooks without seeing missing or divergent session keys. Verification: - gh pr checks 73706 --repo openclaw/openclaw --watch=false - Real behavior proof: https://github.com/openclaw/openclaw/actions/runs/26526635074/job/78131933497 Thanks @zeroaltitude. Co-authored-by: Edward Abrams <[email protected]>
Thread the canonical outbound session key into plugin message_sending and message_sent hook contexts, and align native command redirect routed delivery with the agent runtime session key. This lets plugins correlate agent_end with outbound delivery hooks without seeing missing or divergent session keys. Verification: - gh pr checks 73706 --repo openclaw/openclaw --watch=false - Real behavior proof: https://github.com/openclaw/openclaw/actions/runs/26526635074/job/78131933497 Thanks @zeroaltitude. Co-authored-by: Edward Abrams <[email protected]>
What
Three layers of one bug:
Plumbing fix.
applyMessageSendingHookinsrc/infra/outbound/deliver.tsconstructed thePluginHookMessageContextwith only{ channelId, accountId, conversationId }and dropped thesessionKeythatdeliverOutboundPayloadsalready had in scope assessionKeyForInternalHooks(resolved at deliver.ts:952 frommirror.sessionKey ?? session.key). Plugins observingmessage_sendingsawctx.sessionKey === undefinedfor every reply.Semantic fix.
dispatch-from-config.ts:488was fillingsession.keywithctx.SessionKeyunconditionally. For non-native chat that's correct (matches what the agent runner uses); for native-command-redirect (CommandTargetSessionKeyset), the agent runs against the redirect target, sosession.keymust follow it too. Otherwiseagent_endandmessage_sendingsee different sessionKeys for the same turn. Now mirrorsget-reply.ts:198'sagentSessionKey = targetSessionKey || ctx.SessionKey.Contract documentation. Tightened JSDoc on
OutboundSessionContext.key/policyKeyandPluginHookMessageContext.sessionKey/runIdso the agent_end ↔ message_sending correlation invariant is documented, and added explicit comments indeliver.tsdistinguishing the diagnostics fallback (?? policyKeyallowed) from the internal-hook fallback (?? policyKeydeliberately disallowed).Concrete regression observed
A non-bundled plugin (
openclaw-provenance) correlates a per-turn signal across two hooks:agent_endwrites to a per-session map keyed byctx.sessionKey.message_sendinglooks up the same key and appends a developer-mode taint footer to outbound content.Pre-fix, message_sending's ctx.sessionKey was
undefined(issue 1) so the lookup always missed; post-plumbing-fix but pre-semantic-fix, the lookup would also miss for any native-command-redirect flow (issue 2). Both regressions are addressed; downstream plugins now have a stable invariant (issue 3).Tests
pnpm test src/infra/outbound/deliver.test.ts→ 56/56 (53 prior + 3 new). New tests cover:tsgo --noEmittypecheck clean.Risk
Low. The plumbing fix is additive (
sessionKey?: stringfield already declared in the hook context type). The semantic fix only changes behavior for the native-command-redirect path, which was already broken (no plugin could correlate across hooks there). JSDoc/comment updates are documentation-only. Five files modified at narrow points.Real behavior proof
Behavior or issue addressed: Canonical outbound
sessionKeyis threaded intomessage_sending/message_senthook context, including native redirect session-key selection from PR fix(outbound): thread sessionKey into message_sending + align session.key with agent runtime + document the contract #73706.Real environment tested: Local OpenClaw topic branch
fix/message-sending-session-keyat862bb66603, realdeliverOutboundPayloads, realgetGlobalHookRunner, real registered hooks, real channel plugin path viascripts/proof-73706-message-sending-session-key.ts.Exact steps or command run after this patch: Ran
pnpm tsx scripts/proof-73706-message-sending-session-key.tsafter the patch and captured the runtime hook context values received by the registered hooks.Evidence after fix: Full copied runtime output is in the proof comment: fix(outbound): thread sessionKey into message_sending + align session.key with agent runtime + document the contract #73706 (comment) and saved locally at
~/reports/proof-73706/run-output.txt.Excerpt of copied live output:
Observed result after fix: The actual runtime hook context contains the canonical delivery session key for outbound hooks; native redirect delivery uses the redirect target key instead of the inbound session key.
What was not tested:
runIdoutbound hook correlation is documented as not yet plumbed and was not claimed/tested by this PR.