Skip to content

fix(hooks): deduplicate after_tool_call hook in embedded runs [AI-assisted]#27283

Closed
jbeno wants to merge 1 commit intoopenclaw:mainfrom
jbeno:fix/dedupe-after-tool-call-hook
Closed

fix(hooks): deduplicate after_tool_call hook in embedded runs [AI-assisted]#27283
jbeno wants to merge 1 commit intoopenclaw:mainfrom
jbeno:fix/dedupe-after-tool-call-hook

Conversation

@jbeno
Copy link
Contributor

@jbeno jbeno commented Feb 26, 2026

Summary

  • Problem: The after_tool_call plugin hook fires twice per tool execution in embedded agent runs. Two independent code paths both dispatch the hook: pi-tool-definition-adapter.ts (the tool adapter wrapper) and handleToolExecutionEnd in pi-embedded-subscribe.handlers.tools.ts (the subscription event handler).
  • Why it matters: Plugin authors observing after_tool_call see duplicate events for every tool call, forcing them to implement client-side deduplication workarounds (windowed hash maps, etc.). This is a correctness bug in the hook lifecycle.
  • What changed: Removed the after_tool_call dispatch from pi-tool-definition-adapter.ts. The subscription handler (handleToolExecutionEnd) is now the single source of truth — it already has richer context (duration, sanitized result, session info). Added regression tests and an integration test that verifies the hook fires exactly once.
  • What did NOT change (scope boundary): before_tool_call behavior is untouched (it was already deduplicated in PR fix(hooks): fire before_tool_call hook exactly once per tool invocation (#15502) #15635). The subscription handler dispatch logic is unchanged. consumeAdjustedParamsForToolCall cleanup remains in the adapter to prevent memory leaks from before_tool_call parameter tracking.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

User-visible / Behavior Changes

Plugins listening on after_tool_call will now receive exactly one event per tool execution instead of two. Plugins with deduplication workarounds will continue to work but the workarounds become unnecessary.

Security Impact (required)

  • New permissions/capabilities? No
  • Secrets/tokens handling changed? No
  • New/changed network calls? No
  • Command/tool execution surface changed? No
  • Data access scope changed? No

Repro + Verification

Environment

  • OS: macOS 15 (darwin 24.6.0)
  • Runtime/container: Node 22+, Bun
  • Model/provider: Any (hook fires regardless of model)
  • Integration/channel (if any): Any plugin using after_tool_call

Steps

  1. Install a plugin that logs after_tool_call events
  2. Run an embedded agent session (e.g. openclaw agent)
  3. Trigger any tool execution (e.g. Read, Bash)

Expected

  • Plugin receives exactly 1 after_tool_call event per tool execution

Actual (on main)

  • Plugin receives 2 after_tool_call events per tool execution — one from the adapter wrapper, one from the subscription handler

Evidence

  • Failing test/log before + passing after
  • Trace/log snippets

New integration test (pi-tool-definition-adapter.after-tool-call.fires-once.test.ts): simulates the full embedded runtime flow (adapter execute + subscription handler event) and asserts toHaveBeenCalledTimes(1). This test was verified to fail with toHaveBeenCalledTimes(2) when the bug is reintroduced.

Regression guard test (pi-tool-definition-adapter.after-tool-call.test.ts): updated to assert the adapter does NOT call runAfterToolCall, confirming the hook dispatch was removed from the adapter.

Full test suite: 1417 test files, 11613 tests — all passing (pnpm build && pnpm check && pnpm test).

Human Verification (required)

  • Verified scenarios:
    • Ran full test suite (11613 tests pass)
    • Ran the gateway on main with a plugin observing after_tool_call — confirmed 2 events fire per tool call
    • Ran the gateway on this branch with the same plugin — confirmed exactly 1 event fires per tool call
    • Verified the integration test correctly detects the bug: reverting the fix causes the test to fail with count=2
  • Edge cases checked:
    • Tool execution errors (hook still fires once with error info via subscription handler)
    • Sequential tool calls (each fires exactly once, no cross-contamination)
    • consumeAdjustedParamsForToolCall cleanup still runs in the adapter to prevent leaks from before_tool_call parameter tracking
  • What you did not verify:
    • Non-embedded (standalone CLI) runs — after_tool_call dispatch paths differ there and are not affected by this change

Compatibility / Migration

  • Backward compatible? Yes
  • Config/env changes? No
  • Migration needed? No

Failure Recovery (if this breaks)

  • How to disable/revert this change quickly: Revert commit c129a1a74 — it re-adds the after_tool_call dispatch block to pi-tool-definition-adapter.ts
  • Files/config to restore: src/agents/pi-tool-definition-adapter.ts
  • Known bad symptoms reviewers should watch for: If after_tool_call stops firing entirely (zero times), it means the subscription handler path is not being reached — but that would also break before_tool_call which uses the same handler pattern

Risks and Mitigations

  • Risk: The adapter was dispatching after_tool_call with the raw (unsanitized) tool result, while the subscription handler dispatches with the sanitized result. Plugins that depended on seeing the raw result will now only see the sanitized version.
    • Mitigation: The sanitized result is the correct one for plugins to consume (it strips sensitive/internal data). The raw result exposure was unintentional.

AI Disclosure: This PR was built with AI assistance (Cursor + Claude).

Made with Cursor

@openclaw-barnacle openclaw-barnacle bot added agents Agent runtime and tooling size: M labels Feb 26, 2026
@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 26, 2026

Greptile Summary

Removed duplicate after_tool_call hook dispatch from the tool adapter, keeping the subscription handler (handleToolExecutionEnd) as the single source of truth for embedded agent runs.

Key Changes

  • Removed the after_tool_call hook invocation from pi-tool-definition-adapter.ts (both success and error paths)
  • Kept consumeAdjustedParamsForToolCall cleanup to prevent memory leaks from before_tool_call parameter tracking
  • Added comprehensive integration test (pi-tool-definition-adapter.after-tool-call.fires-once.test.ts) that simulates the full embedded runtime flow and verifies exactly one hook invocation
  • Updated existing test to assert the adapter does NOT fire the hook

Impact

Plugin authors observing after_tool_call will now receive exactly one event per tool execution instead of two. The subscription handler provides richer context (duration, sanitized result, session info) compared to the adapter's raw result exposure.

Confidence Score: 5/5

  • Safe to merge - clean fix for correctness bug with comprehensive test coverage
  • Clear root cause identification and surgical fix. Removes duplicate dispatch while maintaining memory leak prevention. Comprehensive integration and regression tests verify the fix. Backward compatible (existing plugin workarounds continue to work). Follows established pattern from before_tool_call deduplication. Full test suite (11613 tests) passing.
  • No files require special attention

Last reviewed commit: c129a1a

@jbeno
Copy link
Contributor Author

jbeno commented Feb 26, 2026

@Patrick-Barletta and @lailoo can you take a look since you've spent some time working on hooks?

@vincentkoc
Copy link
Member

Consolidating this work into #32201 so Option A lands as one mergeable unit.

#32201 includes the single-fire dedupe behavior from this PR and combines it with embedded context propagation (sessionKey + agentId).

Link: #32201

@vincentkoc
Copy link
Member

Superseded by #32201, which is now merged.

This PR's single-fire dedupe behavior is included in the consolidated merge:
#32201

@vincentkoc vincentkoc closed this Mar 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling size: M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants