@@ -28,6 +28,7 @@ import {
2828 runBeforeToolCallHook ,
2929 wrapToolWithBeforeToolCallHook ,
3030} from "./agent-tools.before-tool-call.js" ;
31+ import { normalizeToolParameters } from "./agent-tools.schema.js" ;
3132import { CRITICAL_THRESHOLD } from "./tool-loop-detection.js" ;
3233import type { AnyAgentTool } from "./tools/common.js" ;
3334import { callGatewayTool } from "./tools/gateway.js" ;
@@ -418,6 +419,53 @@ describe("before_tool_call loop detection behavior", () => {
418419 expect ( execute ) . toHaveBeenCalledTimes ( 1 ) ;
419420 } ) ;
420421
422+ it ( "re-wraps a normalized already-wrapped tool with the current context (createOpenClawCodingTools path)" , async ( ) => {
423+ const execute = vi . fn ( ) . mockResolvedValue ( {
424+ content : [ { type : "text" , text : "ok" } ] ,
425+ details : { ok : true } ,
426+ } ) ;
427+ const params = { path : "/tmp/file" } ;
428+
429+ // Step 1: wrap with initial context (shared tool registry)
430+ const firstContext = {
431+ ...enabledLoopDetectionContext ,
432+ runId : "initial-run" ,
433+ sessionKey : "initial-session" ,
434+ } ;
435+ const baseTool : AnyAgentTool = { name : "read" , execute } as any ;
436+ const wrappedOnce = wrapToolWithBeforeToolCallHook (
437+ { name : "read" , execute } as any ,
438+ firstContext ,
439+ ) ;
440+ expect ( isToolWrappedWithBeforeToolCallHook ( wrappedOnce ) ) . toBe ( true ) ;
441+
442+ // Step 2: normalize (simulating createOpenClawCodingTools pipeline)
443+ // normalizeToolParameters does { ...tool, parameters } which drops
444+ // non-enumerable Symbol metadata (BEFORE_TOOL_CALL_SOURCE_TOOL,
445+ // BEFORE_TOOL_CALL_HOOK_CONTEXT). After the fix, those Symbols
446+ // are preserved via Object.getOwnPropertySymbols.
447+ const normalized = normalizeToolParameters ( wrappedOnce , {
448+ modelProvider : "anthropic" ,
449+ } ) ;
450+
451+ // Step 3: re-wrap with new context (the second half of the pipeline)
452+ const newContext = {
453+ ...enabledLoopDetectionContext ,
454+ runId : "new-run" ,
455+ sessionKey : "new-session" ,
456+ } ;
457+ const reWrapped = wrapToolWithBeforeToolCallHook ( normalized , newContext ) ;
458+
459+ // After normalization + re-wrap, the tool should be wrapped with new context
460+ expect ( isToolWrappedWithBeforeToolCallHook ( reWrapped ) ) . toBe ( true ) ;
461+ // The re-wrapped tool is a fresh wrapper, not the normalized one
462+ expect ( reWrapped ) . not . toBe ( normalized ) ;
463+
464+ // Execute: the hook should fire exactly once with the new context
465+ await expectUnblockedToolExecution ( reWrapped , "call-1" , params ) ;
466+ expect ( execute ) . toHaveBeenCalledTimes ( 1 ) ;
467+ } ) ;
468+
421469 it ( "escalates generic repeat diagnostics from warning to critical" , async ( ) => {
422470 await withToolLoopEvents ( async ( emitted ) => {
423471 const { tool, params } = createGenericReadRepeatFixture ( ) ;
0 commit comments