Skip to content

Approval deadlock: approving a held function call re-runs approval::gate (duplicate hook bindings) and parks the turn forever #797

Description

@faramirezs

Summary

In manual approval mode, approving a held function call in the console chat deadlocks the turn: the approval card disappears but the turn stays awaiting_functions forever, with the call re-parked at approval::gate and no pending record left to resolve.

Root cause

Three steps combine:

  1. approval-gate registers duplicate hook bindings. retry_hook_bindings (approval-gate/src/configuration.rs) re-registers the harness::hook::pre-trigger binding for approval::gate whenever engine::triggers::info reports 0 instances. The engine's instance count lags a successful registration, so the retry loop keeps re-binding — observed 171 duplicate pre-trigger instances (and 171 post-trigger) within ~90s of startup, on a fresh stack. main.rs additionally binds directly before starting the loop, guaranteeing at least two.

  2. The harness keeps every instance. The harness's HookSet is keyed by trigger instance id (harness/src/hooks/mod.rs), so all 171 identical approval::gate bindings are consulted in chain order.

  3. On approval, the chain resumes past only the first holder. approval::resolveharness::function::resolve(action="execute")run_pre_trigger(..., resume_after=Some("approval::gate")). chain_slice skips the first matching holder; the second duplicate re-runs the gate. The gate's idempotency re-holds (pending::get → Some → hold, no new record), the harness re-parks, and the resolve then deletes the pending record — leaving an orphaned park with nothing to resolve. "Nothing happens."

Evidence

  • engine::triggers::info {id:"harness::hook::pre-trigger"}instance_count: 171 (one gate startup); harness::hook::post-trigger → 171.
  • Gate startup log: trigger binding requested repeated ~0.5s apart from start until "confirmed" (~90s later).
  • Trace of an approval: approval::resolve ok → harness::function::resolve ok → execute approval::gate{"decision":"hold"}state::set harness_turn (re-park) → pending record deleted. turn_resumed: false.
  • E2E repro (fresh stack, current main code): approve → card clears → turn awaiting_functions indefinitely.

Impact

Any deployment using manual approval mode: the first approval after a gate startup storm deadlocks the turn; the operator must restart the harness.

Suggested fix

  • approval-gate: register each hook binding at most once per startup (retry only failed attempts — harness not up yet), and drop the direct pre-bind in main.rs.
  • harness: dedupe hook bindings by function_id per hook point (first registration wins) so a misbehaving registrar can never double-run a hook.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions