fix(hooks): consolidate after_tool_call context + single-fire behavior#32201
fix(hooks): consolidate after_tool_call context + single-fire behavior#32201vincentkoc merged 10 commits intomainfrom
Conversation
(cherry picked from commit c129a1a)
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 b711738)
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 aad01ed)
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2628a4f41f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Greptile SummaryThis PR consolidates two previously split fixes for the Key changes:
One minor issue: the doc-comment in the new Confidence Score: 4/5
Last reviewed commit: 2628a4f |
src/agents/pi-tool-definition-adapter.after-tool-call.fires-once.test.ts
Outdated
Show resolved
Hide resolved
🔒 Aisle Security AnalysisWe found 1 potential security issue(s) in this PR:
1. 🔵 Cross-session/race-prone adjusted tool params lookup keyed only by toolCallId
Description
This creates a shared mutable state issue:
Vulnerable flow (simplified):
Vulnerable code (new consumption point): const adjustedArgs = consumeAdjustedParamsForToolCall(toolCallId);
const afterToolCallArgs =
adjustedArgs && typeof adjustedArgs === "object"
? (adjustedArgs as Record<string, unknown>)
: startArgs;Underlying shared global state: const adjustedParamsByToolCallId = new Map<string, unknown>();
export function consumeAdjustedParamsForToolCall(toolCallId: string): unknown {
const params = adjustedParamsByToolCallId.get(toolCallId);
adjustedParamsByToolCallId.delete(toolCallId);
return params;
}RecommendationScope adjusted-params storage/consumption to a run/session (or keep it entirely within per-session handler state), and ensure cleanup on abnormal termination. Suggested fix options:
// pi-tools.before-tool-call.ts
const adjustedParamsByKey = new Map<string, unknown>();
const keyFor = (sessionKey: string, toolCallId: string) => `${sessionKey}:${toolCallId}`;
export function storeAdjustedParams(sessionKey: string, toolCallId: string, params: unknown) {
adjustedParamsByKey.set(keyFor(sessionKey, toolCallId), params);
}
export function consumeAdjustedParamsForToolCall(sessionKey: string, toolCallId: string): unknown {
const key = keyFor(sessionKey, toolCallId);
const params = adjustedParamsByKey.get(key);
adjustedParamsByKey.delete(key);
return params;
}// pi-embedded-subscribe.handlers.tools.ts
const adjustedArgs =
ctx.params.sessionKey ? consumeAdjustedParamsForToolCall(ctx.params.sessionKey, toolCallId) : undefined;
Also consider:
Analyzed PR: #32201 at commit Last updated on: 2026-03-02T22:37:42Z |
openclaw#32201) * fix(hooks): deduplicate after_tool_call hook in embedded runs (cherry picked from commit c129a1a) * 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 b711738) * fix(hooks): thread agentId through to after_tool_call hook context Follow-up to openclaw#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 aad01ed) * 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>
openclaw#32201) * fix(hooks): deduplicate after_tool_call hook in embedded runs (cherry picked from commit c129a1a) * 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 b711738) * fix(hooks): thread agentId through to after_tool_call hook context Follow-up to openclaw#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 aad01ed) * 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>
openclaw#32201) * fix(hooks): deduplicate after_tool_call hook in embedded runs (cherry picked from commit c129a1a) * 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 (cherry picked from commit b711738) * fix(hooks): thread agentId through to after_tool_call hook context Follow-up to openclaw#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 (cherry picked from commit aad01ed) * 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>
Summary
after_tool_callhooks could miss session context and/or fire more than once per tool execution.sessionKeypropagation from fix(hooks): thread agentId into after_tool_call hook context #30527agentIdpropagation from fix(hooks): thread agentId into after_tool_call hook context #30527run/attempt.tskeeps normalizedsessionKey: sandboxSessionKeyand addsagentId: sessionAgentIdtoolCallId/runId/sessionIdremain follow-up).AI-assisted: yes
Change Type (select all)
Scope (select all touched areas)
Linked Issue/PR
User-visible / Behavior Changes
after_tool_callin embedded runs now includessessionKeyandagentIdcontext when available.after_tool_callnow fires exactly once per tool execution path in embedded runs (success and error).Security Impact (required)
No)No)No)No)No)Yes, explain risk + mitigation:Repro + Verification
Environment
Steps
after_tool_callhook.Expected
sessionKeyandagentIdin embedded runs.Actual
Evidence
Human Verification (required)
What you personally verified (not just CI), and how:
pnpm checkpnpm exec vitest run src/agents/pi-tool-definition-adapter.after-tool-call.test.ts src/agents/pi-tool-definition-adapter.after-tool-call.fires-once.test.tspnpm exec vitest run --config vitest.e2e.config.ts src/plugins/wired-hooks-after-tool-call.e2e.test.tssessionKey+agentId.pnpm test/ full e2e matrix.Compatibility / Migration
Yes)No)No)Failure Recovery (if this breaks)
src/agents/pi-tool-definition-adapter.tssrc/agents/pi-tool-definition-adapter.after-tool-call.test.tssrc/agents/pi-tool-definition-adapter.after-tool-call.fires-once.test.tssrc/agents/pi-embedded-subscribe.handlers.tools.tssrc/agents/pi-embedded-subscribe.handlers.types.tssrc/agents/pi-embedded-subscribe.types.tssrc/agents/pi-embedded-runner/run/attempt.tssrc/plugins/wired-hooks-after-tool-call.e2e.test.tsCHANGELOG.mdafter_tool_callhook invocations.Risks and Mitigations