{"record":{"id":"da167d5e1c707f3b","repo":"NousResearch/hermes-agent","slug":"hermes-tool-execution-callback-invoked-more-than-o","errorCode":null,"errorMessage":"Hermes tool execution callback invoked more than once","messagePattern":"Hermes tool execution callback invoked more than once","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"agent/tool_executor.py","lineNumber":516,"sourceCode":"    from agent import relay_tools\n    from hermes_cli.middleware import (\n        apply_tool_request_middleware,\n        run_tool_execution_middleware,\n    )\n\n    trace = middleware_trace if middleware_trace is not None else []\n    state = {\n        \"args\": function_args,\n        \"middleware_trace\": trace,\n        \"blocked\": False,\n        \"dispatched\": False,\n    }\n    dispatch_lock = threading.Lock()\n\n    def _authorized_dispatch(final_args: dict[str, Any]) -> Any:\n        with dispatch_lock:\n            if state[\"dispatched\"]:\n                raise RuntimeError(\n                    \"Hermes tool execution callback invoked more than once\"\n                )\n            state[\"dispatched\"] = True\n            state[\"blocked\"] = False\n            state[\"args\"] = final_args\n\n        def _begin() -> None:\n            _begin_tool_execution(\n                agent,\n                function_name=function_name,\n                function_args=final_args,\n                effective_task_id=effective_task_id,\n                tool_call_id=tool_call_id,\n                display_index=display_index,\n            )\n\n        def _advance_start_order(callback=None) -> None:\n            if begin_execution is None:","sourceCodeStart":498,"sourceCodeEnd":534,"githubUrl":"https://github.com/NousResearch/hermes-agent/blob/c896c09c42910c584c4c7d2325b58c14713ea42c/agent/tool_executor.py#L498-L534","documentation":"Raised in agent/tool_executor.py's authorized-dispatch closure: the middleware pipeline may invoke the final execution callback exactly once, enforced under a dispatch lock with a state['dispatched'] flag. A second invocation — typically a middleware calling next()/callback twice, or an authorization middleware both dispatching and re-dispatching after post-processing — trips this RuntimeError to surface the double-execution bug.","triggerScenarios":"A custom tool-execution middleware that calls the dispatch callback again after inspecting results (e.g. retry-on-failure implemented at the wrong layer); a middleware chain where two branches both forward the callback; looping middleware that re-enters the continuation on timeout; SDK upgrades changing callback arity such that a wrapper forwards twice.","commonSituations":"Adding a custom middleware (logging, audit, retry) around Hermes tool execution; porting Express/Koa-style 'next()' habits where double-next is tolerated; a bug where an exception path falls through into a second call.","solutions":["Audit your middleware: the dispatch callback must be invoked on exactly one control-flow path — guard it with a local 'called' flag if unsure.","Implement retries above the middleware layer (wrap the whole tool call), not by re-invoking the callback inside middleware.","Remove or fix the offending middleware and reproduce with a minimal chain of one custom middleware plus defaults.","If you ship no custom middleware and still hit this, report it as a Hermes core bug with the middleware stack dump (middleware_trace is captured in state)."],"exampleFix":"# before (middleware re-dispatching)\ndef my_middleware(args, next):\n    result = next(args)\n    if result_needs_retry(result):\n        return next(args)  # second call -> RuntimeError\n    return result\n\n# after\ndef my_middleware(args, next):\n    return next(args)  # retry belongs outside; wrap the outer call instead","handlingStrategy":"type-guard","validationCode":"def make_safe_middleware(mw):\n    called = False\n    def wrapper(args, next):\n        nonlocal called\n        if called:\n            raise RuntimeError(\"double dispatch detected before Hermes does\")\n        called = True\n        return mw(args, next)\n    return wrapper\n# register wrapper-wrapped middleware so double-invocation fails fast in YOUR code","typeGuard":null,"tryCatchPattern":"try:\n    result = execute_tool_with_middleware(...)\nexcept RuntimeError as exc:\n    if \"invoked more than once\" in str(exc):\n        disable_offending_middleware_and_retry_without_it()\n    else:\n        raise","preventionTips":["Invoke the dispatch callback on exactly one control-flow path; guard with a local called flag.","Implement retries by wrapping the entire tool call from outside, never by re-calling next()/callback inside middleware.","Test each custom middleware in isolation before stacking it on the default chain.","On upgrades, re-read the middleware contract — arity and single-invocation rules are enforced strictly."],"tags":["tools","middleware","execution","invariant"],"backgroundTag":null,"analyzedSha":"c896c09c42910c584c4c7d2325b58c14713ea42c","analyzedAt":"2026-08-14T17:18:01.089Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}