Skip to content

fix(outbound): thread sessionKey into message_sending + align session.key with agent runtime + document the contract#73706

Merged
steipete merged 19 commits into
openclaw:mainfrom
zeroaltitude:fix/message-sending-session-key
May 27, 2026
Merged

fix(outbound): thread sessionKey into message_sending + align session.key with agent runtime + document the contract#73706
steipete merged 19 commits into
openclaw:mainfrom
zeroaltitude:fix/message-sending-session-key

Conversation

@zeroaltitude

@zeroaltitude zeroaltitude commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

What

Three layers of one bug:

  1. Plumbing fix. applyMessageSendingHook in src/infra/outbound/deliver.ts constructed the PluginHookMessageContext with only { channelId, accountId, conversationId } and dropped the sessionKey that deliverOutboundPayloads already had in scope as sessionKeyForInternalHooks (resolved at deliver.ts:952 from mirror.sessionKey ?? session.key). Plugins observing message_sending saw ctx.sessionKey === undefined for every reply.

  2. Semantic fix. dispatch-from-config.ts:488 was filling session.key with ctx.SessionKey unconditionally. For non-native chat that's correct (matches what the agent runner uses); for native-command-redirect (CommandTargetSessionKey set), the agent runs against the redirect target, so session.key must follow it too. Otherwise agent_end and message_sending see different sessionKeys for the same turn. Now mirrors get-reply.ts:198's agentSessionKey = targetSessionKey || ctx.SessionKey.

  3. Contract documentation. Tightened JSDoc on OutboundSessionContext.key/policyKey and PluginHookMessageContext.sessionKey/runId so the agent_end ↔ message_sending correlation invariant is documented, and added explicit comments in deliver.ts distinguishing the diagnostics fallback (?? policyKey allowed) from the internal-hook fallback (?? policyKey deliberately disallowed).

Concrete regression observed

A non-bundled plugin (openclaw-provenance) correlates a per-turn signal across two hooks:

  • agent_end writes to a per-session map keyed by ctx.sessionKey.
  • message_sending looks 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:

  • sessionKey threaded into ctx when session is provided
  • sessionKey absent when session is absent
  • session.key (canonical) wins over session.policyKey in the hook ctx

tsgo --noEmit typecheck clean.

Risk

Low. The plumbing fix is additive (sessionKey?: string field 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 sessionKey is threaded into message_sending / message_sent hook 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-key at 862bb66603, real deliverOutboundPayloads, real getGlobalHookRunner, real registered hooks, real channel plugin path via scripts/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.ts after 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:

    pnpm tsx scripts/proof-73706-message-sending-session-key.ts
    scenario: direct outbound delivery
    message_sending ctx.sessionKey = agent:proof:direct
    message_sent ctx.sessionKey = agent:proof:direct
    scenario: native redirect target
    message_sending ctx.sessionKey = agent:proof:redirect-target
    message_sent ctx.sessionKey = agent:proof:redirect-target
    proof: PASS
    
  • 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: runId outbound hook correlation is documented as not yet plumbed and was not claimed/tested by this PR.

@greptile-apps

greptile-apps Bot commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a three-part bug where sessionKey was not threaded into the message_sending hook context, and where session.key diverged from the agent runtime's params.sessionKey on native-command-redirect flows. The fix is additive and narrow: sessionKeyForInternalHooks is plumbed into applyMessageSendingHook, dispatch-from-config.ts mirrors get-reply.ts's targetSessionKey || ctx.SessionKey resolution, and JSDoc tightens the invariant contract. Three new tests directly validate the corrected behavior.

Confidence Score: 5/5

This PR is safe to merge — changes are additive, well-tested, and confined to narrow plumbing points.

No P0 or P1 issues found. The ?? vs || difference between dispatch-from-config.ts and get-reply.ts is semantically equivalent because normalizeOptionalString never returns an empty string (it collapses to undefined for blank/whitespace input). All three fix layers are coherent, the 56/56 test suite passes, and types are clean.

No files require special attention.

Reviews (2): Last reviewed commit: "fix(outbound): align session.key with ag..." | Re-trigger Greptile

@clawsweeper

clawsweeper Bot commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge. Reviewed May 27, 2026, 1:10 PM ET / 17:10 UTC.

Summary
This PR threads canonical outbound sessionKey into message_sending and message_sent plugin hook contexts, aligns native routed replies with the agent runtime session key, adds contract docs, regression tests, and a proof script.

PR surface: Source +93, Tests +274, Other +193. Total +560 across 7 files.

Reproducibility: yes. Source inspection shows current main has sessionKeyForInternalHooks in scope but does not pass it to plugin outbound contexts, and routed delivery still uses ctx.SessionKey where the agent runtime uses the native target key.

Review metrics: 2 noteworthy metrics.

  • Plugin hook contract surface: 1 optional field populated across 2 outbound hooks. External plugins can start depending on ctx.sessionKey/event.sessionKey for message_sending and message_sent, so maintainers should notice the contract becoming operational.
  • Routed session-key branch: 1 native-command redirect branch changed. The branch now aligns routed outbound delivery with the agent runtime session key, which is correct for correlation but changes plugin-observable session identity.

Merge readiness
Overall: 🦞 diamond lobster
Proof: 🦞 diamond lobster
Patch quality: 🦞 diamond lobster
Result: ready for maintainer review.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Risk before merge

  • Merging changes the canonical sessionKey visible to plugins on native-command redirected outbound delivery from the inbound session to the redirect target session; that is the intended correlation fix, but it is still a session-state contract decision for external plugin authors.
  • The plugin hook context becomes a more explicit public contract for sessionKey on outbound hooks, so maintainers should be comfortable with the documented semantics before merge.

Maintainer options:

  1. Accept the session-key contract (recommended)
    Land if maintainers agree that outbound plugin hooks should expose the same native redirect target session key that the agent runtime used for the turn.
  2. Keep legacy routed-key behavior
    Ask for a narrower patch that only threads available session keys into hook contexts while deferring the native redirect semantic change to a separate product/API decision.

Next step before merge
No automated repair is needed; the remaining action is maintainer acceptance of the plugin/session-state contract and normal merge handling.

Security
Cleared: No concrete security or supply-chain issue was found; the PR adds a local proof script and changes runtime hook context plumbing without new dependencies, downloads, secrets handling, or permissions.

Review details

Best possible solution:

Land this PR after maintainer acceptance of the outbound hook session-key contract, and leave broader outbound hook coverage or runId plumbing to the existing follow-up issues.

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

Yes. Source inspection shows current main has sessionKeyForInternalHooks in scope but does not pass it to plugin outbound contexts, and routed delivery still uses ctx.SessionKey where the agent runtime uses the native target key.

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 changes

Label changes:

  • add merge-risk: 🚨 compatibility: The PR makes an optional plugin hook context field operational and documents semantics that external plugins may treat as a public contract.
  • add proof: sufficient: Contributor real behavior proof is sufficient. The PR supplies copied live output from a real-runtime proof script, and the current head has a successful Real behavior proof check run.
  • add rating: 🦞 diamond lobster: Overall readiness is 🦞 diamond lobster; proof is 🦞 diamond lobster and patch quality is 🦞 diamond lobster.
  • remove rating: 🐚 platinum hermit: Current PR rating is rating: 🦞 diamond lobster, so this older rating label is no longer current.

Label justifications:

  • P2: This is a normal-priority plugin hook/session-state fix with bounded blast radius and clear tests/proof.
  • merge-risk: 🚨 compatibility: The PR makes an optional plugin hook context field operational and documents semantics that external plugins may treat as a public contract.
  • merge-risk: 🚨 session-state: The PR changes which session key outbound hooks receive for native-command redirected replies.
  • rating: 🦞 diamond lobster: Overall readiness is 🦞 diamond lobster; proof is 🦞 diamond lobster and patch quality is 🦞 diamond lobster.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (live_output): The PR supplies copied live output from a real-runtime proof script, and the current head has a successful Real behavior proof check run.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR supplies copied live output from a real-runtime proof script, and the current head has a successful Real behavior proof check run.
Evidence reviewed

PR surface:

Source +93, Tests +274, Other +193. Total +560 across 7 files.

View PR surface stats
Area Files Added Removed Net
Source 4 96 3 +93
Tests 2 274 0 +274
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 1 193 0 +193
Total 7 563 3 +560

Acceptance criteria:

  • Review the current PR checks for head 1a0af36, especially checks-node-auto-reply-reply-dispatch, checks-node-agentic-plugin-sdk, check-test-types, check-prod-types, and Real behavior proof.
  • If maintainers want extra local proof before landing, run the focused tests from the PR body through the repository-approved wrapper path rather than raw Vitest in a Codex worktree.

What I checked:

  • AGENTS policy applied: Root AGENTS.md and scoped guides for scripts/, src/infra/outbound/, and src/plugins/ were read; the plugin API/session-state compatibility guidance drove the merge-risk classification, and the changelog policy avoided re-raising the now-removed release-owned changelog entry. (AGENTS.md:1, 11dfef201f81)
  • Effective merge diff is focused: The GitHub merge ref changes seven files: one proof script, two runtime files, three test/doc-contract files, and no final CHANGELOG.md or generated baseline diff. (d0eaee6a30e1)
  • Outbound hook plumbing in merge result: The merge ref forwards sessionKeyForInternalHooks into message_sent, adds sessionKey to applyMessageSendingHook, and derives sessionKeyForInternalHooks from mirror/session key without falling back to policy key. (src/infra/outbound/deliver.ts:1017, d0eaee6a30e1)
  • Native redirect session-key alignment: The merge ref changes routed delivery to pass the native command target session key as the outbound canonical session key, while keeping non-native routed replies on the inbound session key. (src/auto-reply/reply/dispatch-from-config.ts:1305, d0eaee6a30e1)
  • Current-main behavior shows the original gap: Current main builds the sent canonical context without sessionKey and routed delivery still passes ctx.SessionKey, matching the bug this PR fixes. (src/infra/outbound/deliver.ts:1000, 11dfef201f81)
  • Real behavior proof supplied: The proof script uses real deliverOutboundPayloads, global hook runner, registered hooks, and a real channel-plugin path, then asserts direct, no-session, and native-redirect sessionKey outcomes; the PR body/comment include copied live output ending in a pass. (scripts/proof-73706-message-sending-session-key.ts:1, d0eaee6a30e1)

Likely related people:

  • Mariano: Current main blame for the central outbound, dispatch, and hook-context files points to commit f3fe48e, which refreshed the relevant surfaces in the recent durable Telegram send-message work. (role: recent area contributor; confidence: medium; commits: f3fe48e8b791; files: src/infra/outbound/deliver.ts, src/auto-reply/reply/dispatch-from-config.ts, src/plugins/hook-message.types.ts)
  • Agustin Rivera: Commit 48aae82 recently touched outbound delivery/session replay behavior, which is adjacent to the session context this PR threads through hooks. (role: adjacent outbound session contributor; confidence: medium; commits: 48aae82bbc19; files: src/infra/outbound/deliver.ts)
  • joshavant: Commit c476409 centralized outbound payload normalization in deliver.ts, part of the same delivery path now receiving session-key plumbing. (role: outbound delivery refactor contributor; confidence: medium; commits: c4764095f859; files: src/infra/outbound/deliver.ts)
  • Peter Steinberger: Recent history shows core/outbound seam and release work touching nearby runtime boundaries, making this a plausible routing candidate for contract and release-safety review. (role: recent adjacent core contributor; confidence: low; commits: 856592cf001b, 10ad3aa16068; files: src/infra/outbound/deliver.ts, CHANGELOG.md)
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.

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.

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.
@zeroaltitude
zeroaltitude force-pushed the fix/message-sending-session-key branch from 92b5c0b to df71f5f Compare April 28, 2026 17:42
…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.
@zeroaltitude zeroaltitude changed the title fix(outbound): thread sessionKey into message_sending hook context fix(outbound): thread sessionKey into message_sending + align session.key with agent runtime + document the contract Apr 28, 2026
@zeroaltitude

Copy link
Copy Markdown
Contributor Author

@greptile-apps: We have added some documentation to fully explain the hook surface contract regarding session key vs policy key. please re-review.

zeroaltitude added a commit to zeroaltitude/openclaw-plugins that referenced this pull request Apr 28, 2026
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.
zeroaltitude added a commit to zeroaltitude/openclaw-plugins that referenced this pull request Apr 28, 2026
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.
@zeroaltitude
zeroaltitude force-pushed the fix/message-sending-session-key branch from 27516b5 to b4ecfe2 Compare May 5, 2026 04:28
@openclaw-barnacle openclaw-barnacle Bot added the docs Improvements or additions to documentation label May 5, 2026
@openclaw-barnacle openclaw-barnacle Bot added the triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. label May 5, 2026
…(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
@openclaw-barnacle openclaw-barnacle Bot added scripts Repository scripts size: L and removed size: M labels May 6, 2026
@zeroaltitude

Copy link
Copy Markdown
Contributor Author

Round 4 — addressing Clawsweeper review feedback

Round-4 commit: 862bb66603 (force-push not used; this is a follow-up commit on top of the existing branch, per repo policy).

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 — src/plugins/hook-message.types.ts:19-34

Problem (paraphrased). The new JSDoc said outbound message_sending and message_sent both mirror OutboundSessionContext.key, and recommended runId for agent_endmessage_sending correlation. In practice this PR only threaded sessionKey into message_sending; createMessageSentEmitter still built the sent context without sessionKey or runId, and runId was not plumbed through outbound delivery at all. The contract was overstated for message_sent and outright false for runId on outbound hooks.

Solution.

  1. Threaded the canonical outbound sessionKey into the message_sent hook context too. createMessageSentEmitter already takes sessionKeyForInternalHooks (the same value deliverOutboundPayloadsCore computes from params.mirror?.sessionKey ?? params.session?.key for the internal message:sent hook); the canonical context now mirrors that into the plugin context. This makes the message_sending and message_sent contracts symmetric and matches the value the internal hook fires with.
  2. Narrowed the JSDoc on PluginHookMessageContext.runId to honestly describe what is plumbed today: inbound message hooks and agent-runtime hooks (agent_end, llm_input, llm_output) carry runId; outbound message_sending / message_sent do not yet, so plugins should rely on sessionKey for outbound→inbound correlation today (with the documented caveat that it cannot disambiguate concurrent turns in the same session).
  3. Narrowed the JSDoc on PluginHookMessageContext.sessionKey to call out that the field is omitted when delivery has no resolvable session (e.g. internal smoke runs), matching the existing "WITHOUT session" code path.

Result. Commit 862bb66603. Validation:

$ pnpm tsgo:core
... clean

$ pnpm tsgo:core:test
... clean

$ pnpm vitest run src/infra/outbound/deliver.test.ts
 ✓ infra ../../src/infra/outbound/deliver.test.ts (64 tests) 194ms
 Test Files  1 passed (1)
      Tests  64 passed (64)

$ pnpm vitest run src/hooks/message-hook-mappers.test.ts
 ✓ hooks ../../src/hooks/message-hook-mappers.test.ts (11 tests) 23ms
 Test Files  1 passed (1)
      Tests  11 passed (11)

$ pnpm plugin-sdk:api:check
OK docs/.generated/plugin-sdk-api-baseline.sha256

Two new contract tests in src/infra/outbound/deliver.test.ts pin the message_sent sessionKey behavior so it cannot diverge from message_sending unobserved:

  • threads sessionKey into the message_sent hook context when session is provided
  • omits sessionKey from the message_sent hook context when session is absent

[P3] Add the hook fix to the changelog — src/infra/outbound/deliver.ts:1105

Problem (paraphrased). This PR changes user-facing plugin hook behavior and public hook/session JSDoc but did not update CHANGELOG.md. Repo policy requires user-facing fixes to be recorded under ## Unreleased.

Solution. Added a single Plugins/hooks entry under ## Unreleased### Changes describing both runtime changes (canonical outbound sessionKey threaded into message_sending and message_sent; native-command-redirect routed replies aligned on CommandTargetSessionKey), with the user-visible consequence (plugins correlating per-turn state across agent_end and outbound delivery hooks no longer see the session key drop on direct delivery or diverge on native redirects). Credits @zeroaltitude per the contributor template.

Result. Commit 862bb66603, single line added under ## Unreleased / ### Changes.


[P3] Cover native redirect session-key selection — src/auto-reply/reply/dispatch-from-config.ts:583-586

Problem (paraphrased). This PR changed routed native-command replies to send the redirect target session key, but the added tests only exercised direct outbound delivery. We needed a focused dispatch-from-config regression so agent_end and message_sending cannot diverge again while the new delivery tests still pass.

Solution. Added two paired regression tests in src/auto-reply/reply/dispatch-from-config.test.ts that drive dispatchReplyFromConfig with a context that triggers routing (OriginatingChannel differs from Provider) and assert on the actual routeReply call args:

  1. routes native-command-redirect replies using the redirect target sessionKey for outbound deliveryCommandSource: "native", CommandTargetSessionKey: "agent:main:telegram:direct:999", SessionKey: "agent:main:slack:channel:CHAN1". Asserts routeReply is called with sessionKey: "agent:main:telegram:direct:999" and policySessionKey: "agent:main:telegram:direct:999" — i.e. routed delivery follows the redirect target, matching the agent runtime's params.sessionKey resolution.
  2. routes non-native (text) command replies using the inbound sessionKey for outbound delivery — companion test with the same CommandTargetSessionKey populated but CommandSource: "text". Asserts routeReply keeps sessionKey and policySessionKey set to the inbound SessionKey. This guards against accidental generalization of the native-redirect branch into non-native command flows.

Result. Commit 862bb66603. Validation:

$ pnpm vitest run src/auto-reply/reply/dispatch-from-config.test.ts
 ✓ auto-reply ../../src/auto-reply/reply/dispatch-from-config.test.ts (105 tests) 3671ms
 Test Files  1 passed (1)
      Tests  105 passed (105)

(Was 103 before; both new tests are included in the 105.)


After-fix real OpenClaw/plugin behavior proof

Problem (paraphrased). The PR body reported unit tests and typecheck only and did not include after-fix output from a real OpenClaw / plugin run.

Solution. Committed scripts/proof-73706-message-sending-session-key.ts, a self-checking real-runtime harness that does not use vitest mocks. It wires up the production deliverOutboundPayloads path against:

  • a real PluginRegistry populated with one real channel plugin (createOutboundTestPlugin with a real sendText adapter) and two real plugin hooks (message_sending, message_sent)
  • the real setActivePluginRegistry channel resolution path
  • the real getGlobalHookRunner() / initializeGlobalHookRunner() singleton path (no fake hook runner — same code the live gateway uses)

It then exercises three scenarios end-to-end and asserts on the actual hook ctx received at runtime. (The harness caught a pre-commit local regression where my message_sent change had been reverted by an earlier git checkout HEAD -- .; the unit test mock had been happy with stale mock state, but the real runtime path through getGlobalHookRunner exposed the missing field. So this harness is doing its job as a guard against the exact divergence Clawsweeper flagged.)

Run command and full output (copied verbatim from terminal):

$ pnpm tsx scripts/proof-73706-message-sending-session-key.ts

[proof-73706] Real-runtime behavior proof for outbound session-key threading.
[proof-73706] Production code paths: deliverOutboundPayloads + getGlobalHookRunner.

=== Scenario: outbound delivery WITH session.key (canonical key from agent runtime) ===
deliverOutboundPayloads result: [{"channel":"matrix","messageId":"mx-1778038038060","roomId":"!room:example"}]
[message_sending] ctx.sessionKey = "agent:tank:slack:channel:CHAN1"
[message_sending] full ctx     = {"channelId":"matrix","conversationId":"!room:example","sessionKey":"agent:tank:slack:channel:CHAN1"}
[message_sent] ctx.sessionKey = "agent:tank:slack:channel:CHAN1"
[message_sent] full ctx     = {"channelId":"matrix","conversationId":"!room:example","sessionKey":"agent:tank:slack:channel:CHAN1","messageId":"mx-1778038038060"}

=== Scenario: outbound delivery WITHOUT session (narrowed docs branch) ===
deliverOutboundPayloads result: [{"channel":"matrix","messageId":"mx-1778038038070","roomId":"!room:example"}]
[message_sending] ctx.sessionKey = (undefined)
[message_sending] full ctx     = {"channelId":"matrix","conversationId":"!room:example"}
[message_sent] ctx.sessionKey = (undefined)
[message_sent] full ctx     = {"channelId":"matrix","conversationId":"!room:example","messageId":"mx-1778038038070"}

=== Scenario: native-redirect: session.key = CommandTargetSessionKey (what dispatch-from-config.ts now passes) ===
deliverOutboundPayloads result: [{"channel":"matrix","messageId":"mx-1778038038072","roomId":"!room:example"}]
[message_sending] ctx.sessionKey = "agent:tank:telegram:direct:999"
[message_sending] full ctx     = {"channelId":"matrix","conversationId":"!room:example","sessionKey":"agent:tank:telegram:direct:999"}
[message_sent] ctx.sessionKey = "agent:tank:telegram:direct:999"
[message_sent] full ctx     = {"channelId":"matrix","conversationId":"!room:example","sessionKey":"agent:tank:telegram:direct:999","messageId":"mx-1778038038072"}

[proof-73706] All runtime assertions passed.

What this proof demonstrates against the real runtime (not mocks):

  • Scenario 1 (direct delivery, session present): Both message_sending and message_sent receive ctx.sessionKey === "agent:tank:slack:channel:CHAN1" — the canonical OutboundSessionContext.key value plugins observing agent_end will see for the same turn.
  • Scenario 2 (no session attached): Both hooks omit sessionKey, exercising the narrowed docs branch ("plugins must treat it as optional"). No accidental fallback, no policyKey leakage.
  • Scenario 3 (native redirect target): When dispatch-from-config.ts resolves agentRuntimeSessionKey = CommandTargetSessionKey ?? SessionKey for CommandSource: "native" and forwards it via routeReply (which lands in deliverOutboundPayloads as session.key), both outbound hooks observe the redirect-target session — proving end-to-end that agent_end (fired with the runtime sessionKey) and the outbound hooks now see the same canonical key on native redirects.

The harness is committed at scripts/proof-73706-message-sending-session-key.ts so reviewers and CI can re-run it; it asserts each ctx.sessionKey value and exits non-zero on any regression.


Full validation gate (Round 4)

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
git diff --check                                                    → clean
pnpm tsx scripts/proof-73706-message-sending-session-key.ts         → all 3 runtime assertions pass

Ready for re-review.

@openclaw-barnacle openclaw-barnacle Bot added proof: supplied External PR includes structured after-fix real behavior proof. and removed triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. labels May 6, 2026
@clawsweeper clawsweeper Bot added the proof: sufficient ClawSweeper judged the real behavior proof convincing. label May 6, 2026
@openclaw-barnacle openclaw-barnacle Bot removed the proof: sufficient ClawSweeper judged the real behavior proof convincing. label May 25, 2026
@clawsweeper clawsweeper Bot added the proof: sufficient ClawSweeper judged the real behavior proof convincing. label May 25, 2026
@openclaw-barnacle openclaw-barnacle Bot removed the proof: sufficient ClawSweeper judged the real behavior proof convincing. label May 25, 2026
@clawsweeper clawsweeper Bot added the proof: sufficient ClawSweeper judged the real behavior proof convincing. label May 25, 2026
@zeroaltitude

Copy link
Copy Markdown
Contributor Author

Reviewed the latest ClawSweeper comment and prepared the requested CHANGELOG.md cleanup locally. I am not claiming the PR is fixed yet because my current session cannot produce a signed GPG commit: ~/.openclaw-tank/bin/unlock-gpg.sh presets the agent but the signing self-test fails, and OpenClaw policy requires verified signed commits. I will push the cleanup once signing is fixed.

@openclaw-barnacle openclaw-barnacle Bot removed the proof: sufficient ClawSweeper judged the real behavior proof convincing. label May 27, 2026
@zeroaltitude

Copy link
Copy Markdown
Contributor Author

ClawSweeper changelog finding addressed.

  • [P3] Removed the release-owned CHANGELOG.md entry for this PR.
  • Commit: 1a0af36e25 (signed; local signature status G).
  • Validation: git diff --check passed for the changelog cleanup before commit.

@clawsweeper please re-review.

@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🦞 diamond lobster Very strong PR readiness with only minor maintainer review expected. and removed rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. labels May 27, 2026
@steipete steipete self-assigned this May 27, 2026
@steipete

Copy link
Copy Markdown
Contributor

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:

  • gh pr view 73706 --repo openclaw/openclaw --json number,title,author,headRefOid,baseRefName,mergeStateStatus,mergeable,reviewDecision,maintainerCanModify,additions,deletions,changedFiles,url,closingIssuesReferences,labels
  • gh pr checks 73706 --repo openclaw/openclaw --watch=false

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.

@steipete
steipete merged commit 05db911 into openclaw:main May 27, 2026
121 of 124 checks passed
@steipete

Copy link
Copy Markdown
Contributor

Landed in 05db911.

Thanks @zeroaltitude.

@zeroaltitude
zeroaltitude deleted the fix/message-sending-session-key branch May 28, 2026 05:14
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 28, 2026
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]>
eleboucher pushed a commit to eleboucher/homelab that referenced this pull request May 31, 2026
…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. ([#&#8203;87218](openclaw/openclaw#87218), [#&#8203;86875](openclaw/openclaw#86875), [#&#8203;87409](openclaw/openclaw#87409), [#&#8203;87399](openclaw/openclaw#87399), [#&#8203;87375](openclaw/openclaw#87375), [#&#8203;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. ([#&#8203;73706](openclaw/openclaw#73706), [#&#8203;75670](openclaw/openclaw#75670), [#&#8203;87366](openclaw/openclaw#87366), [#&#8203;87451](openclaw/openclaw#87451), [#&#8203;87334](openclaw/openclaw#87334), [#&#8203;84535](openclaw/openclaw#84535), [#&#8203;82492](openclaw/openclaw#82492), [#&#8203;83304](openclaw/openclaw#83304), [#&#8203;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. ([#&#8203;87367](openclaw/openclaw#87367), [#&#8203;87531](openclaw/openclaw#87531), [#&#8203;87682](openclaw/openclaw#87682), [#&#8203;88096](openclaw/openclaw#88096), [#&#8203;88105](openclaw/openclaw#88105)) Thanks [@&#8203;ngutman](https://github.com/ngutman) and [@&#8203;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. ([#&#8203;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. ([#&#8203;87845](openclaw/openclaw#87845), [#&#8203;87890](openclaw/openclaw#87890), [#&#8203;80775](openclaw/openclaw#80775), [#&#8203;84764](openclaw/openclaw#84764), [#&#8203;87751](openclaw/openclaw#87751), [#&#8203;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. ([#&#8203;87398](openclaw/openclaw#87398), [#&#8203;86281](openclaw/openclaw#86281), [#&#8203;87361](openclaw/openclaw#87361), [#&#8203;88133](openclaw/openclaw#88133), [#&#8203;83655](openclaw/openclaw#83655), [#&#8203;87559](openclaw/openclaw#87559), [#&#8203;88088](openclaw/openclaw#88088), [#&#8203;85924](openclaw/openclaw#85924)) Thanks [@&#8203;vincentkoc](https://github.com/vincentkoc) and [@&#8203;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. ([#&#8203;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. ([#&#8203;87370](openclaw/openclaw#87370), [#&#8203;87372](openclaw/openclaw#87372)) Thanks [@&#8203;RomneyDa](https://github.com/RomneyDa).
- ClawHub: add plugin display names plus skill verification and trust surfaces. ([#&#8203;87354](openclaw/openclaw#87354), [#&#8203;86699](openclaw/openclaw#86699)) Thanks [@&#8203;thewilloftheshadow](https://github.com/thewilloftheshadow) and [@&#8203;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. ([#&#8203;87367](openclaw/openclaw#87367), [#&#8203;88096](openclaw/openclaw#88096), [#&#8203;88105](openclaw/openclaw#88105)) Thanks [@&#8203;Solvely-Colin](https://github.com/Solvely-Colin) and [@&#8203;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. ([#&#8203;87313](openclaw/openclaw#87313), [#&#8203;63050](openclaw/openclaw#63050), [#&#8203;87685](openclaw/openclaw#87685)) Thanks [@&#8203;bdjben](https://github.com/bdjben), [@&#8203;liaoandi](https://github.com/liaoandi), and [@&#8203;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. ([#&#8203;87670](openclaw/openclaw#87670), [#&#8203;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. ([#&#8203;87845](openclaw/openclaw#87845), [#&#8203;87890](openclaw/openclaw#87890), [#&#8203;80775](openclaw/openclaw#80775), [#&#8203;84764](openclaw/openclaw#84764), [#&#8203;87794](openclaw/openclaw#87794)) Thanks [@&#8203;eleqtrizit](https://github.com/eleqtrizit) and [@&#8203;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. ([#&#8203;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. ([#&#8203;82823](openclaw/openclaw#82823), [#&#8203;87165](openclaw/openclaw#87165)) Thanks [@&#8203;piersonr](https://github.com/piersonr) and [@&#8203;RomneyDa](https://github.com/RomneyDa).
- Policy: add policy comparison, ingress-channel conformance, and sandbox-posture conformance checks. ([#&#8203;85572](openclaw/openclaw#85572), [#&#8203;85744](openclaw/openclaw#85744), [#&#8203;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]. ([#&#8203;87150](openclaw/openclaw#87150)) Thanks [@&#8203;pgondhi987](https://github.com/pgondhi987).
- Clarify directive persistence authorization policy \[AI]. ([#&#8203;86369](openclaw/openclaw#86369)) Thanks [@&#8203;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. ([#&#8203;87218](openclaw/openclaw#87218), [#&#8203;86875](openclaw/openclaw#86875), [#&#8203;86123](openclaw/openclaw#86123), [#&#8203;88129](openclaw/openclaw#88129), [#&#8203;87399](openclaw/openclaw#87399), [#&#8203;87375](openclaw/openclaw#87375), [#&#8203;72574](openclaw/openclaw#72574), [#&#8203;87383](openclaw/openclaw#87383), [#&#8203;87400](openclaw/openclaw#87400), [#&#8203;83022](openclaw/openclaw#83022), [#&#8203;87671](openclaw/openclaw#87671), [#&#8203;87738](openclaw/openclaw#87738), [#&#8203;87747](openclaw/openclaw#87747), [#&#8203;87706](openclaw/openclaw#87706), [#&#8203;87546](openclaw/openclaw#87546), [#&#8203;87541](openclaw/openclaw#87541), [#&#8203;81048](openclaw/openclaw#81048)) Thanks [@&#8203;mbelinky](https://github.com/mbelinky), [@&#8203;Alix-007](https://github.com/Alix-007), [@&#8203;luoyanglang](https://github.com/luoyanglang), [@&#8203;yetval](https://github.com/yetval), [@&#8203;sjf](https://github.com/sjf), [@&#8203;joshavant](https://github.com/joshavant), [@&#8203;benjamin1492](https://github.com/benjamin1492), [@&#8203;c19354837](https://github.com/c19354837), [@&#8203;fuller-stack-dev](https://github.com/fuller-stack-dev), [@&#8203;pfrederiksen](https://github.com/pfrederiksen), and [@&#8203;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. ([#&#8203;73706](openclaw/openclaw#73706), [#&#8203;75670](openclaw/openclaw#75670), [#&#8203;87366](openclaw/openclaw#87366), [#&#8203;87451](openclaw/openclaw#87451), [#&#8203;87465](openclaw/openclaw#87465), [#&#8203;87334](openclaw/openclaw#87334), [#&#8203;84535](openclaw/openclaw#84535), [#&#8203;76262](openclaw/openclaw#76262), [#&#8203;83304](openclaw/openclaw#83304), [#&#8203;82492](openclaw/openclaw#82492), [#&#8203;87581](openclaw/openclaw#87581), [#&#8203;77114](openclaw/openclaw#77114), [#&#8203;86426](openclaw/openclaw#86426), [#&#8203;85529](openclaw/openclaw#85529), [#&#8203;87160](openclaw/openclaw#87160)) Thanks [@&#8203;zeroaltitude](https://github.com/zeroaltitude), [@&#8203;lukeboyett](https://github.com/lukeboyett), [@&#8203;jarvis-mns1](https://github.com/jarvis-mns1), [@&#8203;xiaotian](https://github.com/xiaotian), [@&#8203;funmerlin](https://github.com/funmerlin), [@&#8203;joshavant](https://github.com/joshavant), [@&#8203;eleqtrizit](https://github.com/eleqtrizit), [@&#8203;heyitsaamir](https://github.com/heyitsaamir), [@&#8203;amittell](https://github.com/amittell), [@&#8203;lidge-jun](https://github.com/lidge-jun), [@&#8203;liorb-mountapps](https://github.com/liorb-mountapps), [@&#8203;masatohoshino](https://github.com/masatohoshino), [@&#8203;bladin](https://github.com/bladin), and [@&#8203;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. ([#&#8203;87398](openclaw/openclaw#87398), [#&#8203;86281](openclaw/openclaw#86281), [#&#8203;87361](openclaw/openclaw#87361), [#&#8203;88133](openclaw/openclaw#88133), [#&#8203;83655](openclaw/openclaw#83655), [#&#8203;87559](openclaw/openclaw#87559), [#&#8203;87719](openclaw/openclaw#87719), [#&#8203;88088](openclaw/openclaw#88088), [#&#8203;85924](openclaw/openclaw#85924), [#&#8203;84362](openclaw/openclaw#84362)) Thanks [@&#8203;Patrick-Erichsen](https://github.com/Patrick-Erichsen), [@&#8203;samzong](https://github.com/samzong), [@&#8203;giodl73-repo](https://github.com/giodl73-repo), [@&#8203;alkor2000](https://github.com/alkor2000), [@&#8203;mmaps](https://github.com/mmaps), [@&#8203;nxmxbbd](https://github.com/nxmxbbd), and [@&#8203;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. ([#&#8203;87810](openclaw/openclaw#87810), [#&#8203;87833](openclaw/openclaw#87833), [#&#8203;75089](openclaw/openclaw#75089)) Thanks [@&#8203;joshavant](https://github.com/joshavant) and [@&#8203;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. ([#&#8203;87883](openclaw/openclaw#87883)) Thanks [@&#8203;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. ([#&#8203;82887](openclaw/openclaw#82887)) Thanks [@&#8203;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. ([#&#8203;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. ([#&#8203;82492](openclaw/openclaw#82492)) Thanks [@&#8203;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. ([#&#8203;87476](openclaw/openclaw#87476), [#&#8203;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. ([#&#8203;86611](openclaw/openclaw#86611)) Thanks [@&#8203;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. ([#&#8203;87593](openclaw/openclaw#87593), [#&#8203;87493](openclaw/openclaw#87493), [#&#8203;80775](openclaw/openclaw#80775), [#&#8203;84764](openclaw/openclaw#84764)) Thanks [@&#8203;Pluviobyte](https://github.com/Pluviobyte) and [@&#8203;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. ([#&#8203;87523](openclaw/openclaw#87523), [#&#8203;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. ([#&#8203;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. ([#&#8203;87647](openclaw/openclaw#87647), [#&#8203;87477](openclaw/openclaw#87477)) Thanks [@&#8203;rohitjavvadi](https://github.com/rohitjavvadi) and [@&#8203;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. ([#&#8203;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
SYU8384 pushed a commit to SYU8384/openclaw that referenced this pull request Jun 3, 2026
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]>
sablehead pushed a commit to sablehead/openclaw that referenced this pull request Jun 10, 2026
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]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-risk: 🚨 session-state 🚨 May lose, corrupt, stale, or mis-associate session, agent, or context state. P2 Normal backlog priority with limited blast radius. proof: sufficient ClawSweeper judged the real behavior proof convincing. proof: supplied External PR includes structured after-fix real behavior proof. rating: 🦞 diamond lobster Very strong PR readiness with only minor maintainer review expected. scripts Repository scripts size: L status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants