* fix(plugins): expose ephemeral sessionId in tool contexts for per-conversation isolation
The plugin tool context (`OpenClawPluginToolContext`) and tool hook
context (`PluginHookToolContext`) only provided `sessionKey`, which
is a durable channel identifier that survives /new and /reset.
Plugins like mem0 that need per-conversation isolation (e.g. mapping
Mem0 `run_id`) had no way to distinguish between conversations,
causing session-scoped memories to persist unbounded across resets.
Add `sessionId` (ephemeral UUID regenerated on /new and /reset) to:
- `OpenClawPluginToolContext` (factory context for plugin tools)
- `PluginHookToolContext` (before_tool_call / after_tool_call hooks)
- Internal `HookContext` for tool call wrappers
Thread the value from the run attempt through createOpenClawCodingTools
→ createOpenClawTools → resolvePluginTools and through the tool hook
wrapper.
Closes#31253
Made-with: Cursor
* fix(agents): propagate embedded sessionId through tool hook context
* test(hooks): cover sessionId in embedded tool hook contexts
* docs(changelog): add sessionId hook context follow-up note
* test(hooks): avoid toolCallId collision in after_tool_call e2e
---------
Co-authored-by: SidQin-cyber <sidqin0410@gmail.com>
* fix(hooks): deduplicate after_tool_call hook in embedded runs
(cherry picked from commit c129a1a74b)
* fix(hooks): propagate sessionKey in after_tool_call context
The after_tool_call hook in handleToolExecutionEnd was passing
`sessionKey: undefined` in the ToolContext, even though the value is
available on ctx.params. This broke plugins that need session context
in after_tool_call handlers (e.g., for per-session audit trails or
security logging).
- Add `sessionKey` to the `ToolHandlerParams` Pick type
- Pass `ctx.params.sessionKey` through to the hook context
- Add test assertion to prevent regression
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
(cherry picked from commit b7117384fc)
* fix(hooks): thread agentId through to after_tool_call hook context
Follow-up to #30511 — the after_tool_call hook context was passing
`agentId: undefined` because SubscribeEmbeddedPiSessionParams did not
carry the agent identity. This threads sessionAgentId (resolved in
attempt.ts) through the session params into the tool handler context,
giving plugins accurate agent-scoped context for both before_tool_call
and after_tool_call hooks.
Changes:
- Add `agentId?: string` to SubscribeEmbeddedPiSessionParams
- Add "agentId" to ToolHandlerParams Pick type
- Pass `agentId: sessionAgentId` at the subscribeEmbeddedPiSession()
call site in attempt.ts
- Wire ctx.params.agentId into the after_tool_call hook context
- Update tests to assert agentId propagation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
(cherry picked from commit aad01edd3e)
* changelog: credit after_tool_call hook contributors
* Update CHANGELOG.md
* agents: preserve adjusted params until tool end
* agents: emit after_tool_call with adjusted args
* tests: cover adjusted after_tool_call params
* tests: align adapter after_tool_call expectation
---------
Co-authored-by: jbeno <jim@jimbeno.net>
Co-authored-by: scoootscooob <zhentongfan@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
handleToolExecutionStart() flushed pending block replies and then called
onBlockReplyFlush() as fire-and-forget (`void`). This created a race where
fast tool results (especially media on Telegram) could be delivered before
the text block that preceded the tool call.
Await onBlockReplyFlush() so the block pipeline finishes before tool
execution continues, preserving delivery order.
Fixes#25267
Co-authored-by: Cursor <cursoragent@cursor.com>
Wire media URL tracking through the embedded agent pipeline so that
media already sent via messaging tools is not delivered again by the
reply dispatcher.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix missing before_tool_call hook integration
- Add hook call in handleToolExecutionStart before tool execution begins
- Support parameter modification via hookResult.params
- Support tool call blocking via hookResult.block with custom blockReason
- Fix try/catch logic to properly re-throw blocking errors using __isHookBlocking flag
- Maintain tool event consistency by emitting start/end events when blocked
- Addresses GitHub issue #6535 (1 of 8 unimplemented hooks now working)
Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com>
* Add comprehensive test suite for before_tool_call hook
- 9 tests covering all hook scenarios: no hooks, parameter passing, modification, blocking, error handling
- Tests tool name normalization and different argument types
- Verifies proper error re-throwing and logging behavior
- Maintained in fork for regression testing
* Fix all issues identified by Greptile code review
Address P0/P1/P3 bugs:
P0 - Fix parameter mutation crash for non-object args:
- Normalize args to objects before passing to hooks (maintains hook contract)
- Handle parameter merging safely for both object and non-object args
P1 - Add missing internal state updates when blocking tools:
- Set toolMetaById metadata like normal flow
- Call onAgentEvent callback to maintain consistency
- Emit events in same order as normal tool execution
P1 - Fix test expectations to match implementation reality:
- Non-object args normalized to {} for hook params (not passed as-is)
- Add test for safe parameter modification with various arg types
- Update mocks to verify state updates when blocking
P3 - Replace magic __isHookBlocking property with dedicated ToolBlockedError class:
- More robust error handling without property collision risk
- Cleaner control flow that's serialization-safe
Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4 <noreply@anthropic.com>
Previously, when block streaming was disabled (the default), text generated
between tool calls would only appear after all tools completed. This was
because onBlockReply wasn't passed to the subscription when block streaming
was off, so flushBlockReplyBuffer() before tool execution did nothing.
Now onBlockReply is always passed, and when block streaming is disabled,
block replies are sent directly during tool flush. Directly sent payloads
are tracked to avoid duplicates in final payloads.
Also fixes a race condition where tool summaries could be emitted before
the typing indicator started by awaiting onAgentEvent in tool handlers.