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:
-
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.
-
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.
-
On approval, the chain resumes past only the first holder. approval::resolve → harness::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.
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_functionsforever, with the call re-parked atapproval::gateand no pending record left to resolve.Root cause
Three steps combine:
approval-gate registers duplicate hook bindings.
retry_hook_bindings(approval-gate/src/configuration.rs) re-registers theharness::hook::pre-triggerbinding forapproval::gatewheneverengine::triggers::inforeports 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.rsadditionally binds directly before starting the loop, guaranteeing at least two.The harness keeps every instance. The harness's
HookSetis keyed by trigger instance id (harness/src/hooks/mod.rs), so all 171 identicalapproval::gatebindings are consulted in chain order.On approval, the chain resumes past only the first holder.
approval::resolve→harness::function::resolve(action="execute")→run_pre_trigger(..., resume_after=Some("approval::gate")).chain_sliceskips 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.trigger binding requestedrepeated ~0.5s apart from start until "confirmed" (~90s later).approval::resolveok →harness::function::resolveok →execute approval::gate→{"decision":"hold"}→state::set harness_turn(re-park) → pending record deleted.turn_resumed: false.awaiting_functionsindefinitely.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