fix(hermes_cli): bound plugin hook callbacks without blocking on timeout (#85125 2d, salvage of #76822) - #93824
Conversation
This is the right architecture for #85125's constraints: abandon-without-join avoids the #6622 shutdown hang, the bounded/fail-closed/caller-thread hook taxonomy is explicitly reasoned rather than accidental, and the token-based cleanup means a late-finishing worker can't clobber a newer invocation's bookkeeping. Points:
Docs ( |
Pull request was closed
Allowlist hot-path hooks for abandon-on-timeout, keep subagent_stop on the caller thread, suppress re-fires of hung callbacks, and block tools when pre_tool_call times out.
5bef9d4 to
a17d6e6
Compare
#85125 Phase 2d. Python plugin hook callbacks ran synchronously on the agent turn hot path with no bound — one wedged callback (network call in an observer, deadlocked lock in a policy hook) froze the conversation loop indefinitely (#76821, #10048). Hook callbacks on the hot-path allowlist are now bounded by
plugins.hook_callback_timeout(default 30s,0disables, clamped at 600), with the worker abandoned, never joined — joining is exactly the #6622 ThreadPoolExecutor-shutdown hang this design avoids.Based on #76822 by @fangliquanflq — cherry-picked to preserve authorship. The PR was a 4-commit stack ~2,600 commits behind main; the tip commit (which contains the final shape of all four) was cherry-picked and the content of the earlier commits that the tip didn't carry (config default, 4 test functions) was restored from the PR diff during conflict resolution, all under his authorship.
Design (his, preserved)
post_tool_call,pre_llm_call,transform_*,pre/post_api_request, session-boundary hooks, …) and the policy hookpre_tool_callare bounded. Teardown/flush hooks (on_session_finalize), policy gates where abandonment is unsafe either way (pre_gateway_dispatch), and observer-only approval/kanban hooks stay unbounded — each exclusion documented in the code.pre_tool_callfails CLOSED: a timed-out or still-running policy callback blocks the tool rather than letting it run without a policy decision.subagent_stop(documented parent-thread serialization) is never moved onto a timeout worker.Salvage-round conflict resolution (main drifted heavily)
invoke_hookon main now routes callbacks through_invoke_hook_callback(additive-payload signature filtering for narrow legacy callbacks, feat(plugins): gateway UX observer hooks + capability-gated platform actions #64176-adjacent). The timeout worker routes through the same helper, so signature filtering applies on both the bounded and unbounded paths — the PR's originalcb(**kwargs)would have regressed narrow-signature callbacks.__init__and force-rediscovery restructure kept intact; the PR's hook-timeout state cleanly added alongside.gateway_platform_event's telemetry-marker exemption (feat(plugins): gateway UX observer hooks + capability-gated platform actions #64176) preserved.Verification
pre_tool_callfail-closed, and an E2E proving a timed-outpre_tool_callblockshandle_function_callbefore dispatchpre_tool_call→ block directive in 0.30s;on_session_finalizeconfirmed synchronous on the caller threadCloses #76822. Fixes #76821. Fixes #10048.
Part of #85125 (Phase 2d).