Skip to content

approvals: a parked call is refused rather than suspended, so approving costs a re-dispatch #561

Description

@oxoxDev

Symptom

Approving a parked call does not make the call happen. The agent is asked to do it again.

Mechanism

Documented in src/harness/policy.rs and worth quoting because the shape is not obvious:

openhuman's ToolPolicy returns RequireApproval, and the session turn loop treats that fail-closed — it blocks the tool call and feeds the model a refusal, rather than suspending the call and resuming it once a verdict arrives. By the time the operator sees the card, the blocked call is gone.

So approval cannot resume anything. Instead it re-issues: approving mints a single-use GrantedCall scoped to that agent, tool and exact arguments, and HarnessBrain re-dispatches the granting agent with an instruction to make the call again unchanged. An unredeemed grant expires on a TTL and the operator is told the agent did not act.

Consequences the operator actually experiences:

  • The turn visibly dead-ends. Work stops, output is partial, and the model may narrate a refusal it was handed.
  • Approval costs a full re-dispatch — more tokens, more latency, and a model free to make different choices the second time even though the grant pins the arguments.
  • A turn that parks several calls loses all of them, not just the first.
  • MAX_APPROVAL_REQUESTS_PER_TURN is 8 and the rest of a turn's parked calls are discarded, so beyond eight the operator is not merely delayed — they never see the request at all.

Why this is the deep one

The block sits in openhuman's turn loop, not here. Options, roughly in ascending cost:

  1. Reduce how often we land here. The other sub-issues do this; it does not fix the mechanism.
  2. Batch the re-dispatch. Approve N cards, re-issue once with all N grants live, rather than a round trip per card.
  3. Suspend and resume properly — an await point in the turn loop, upstream in openhuman. The real fix, and the expensive one.

Worth scoping option 3 before committing to it, and worth noting that option 2 alone would remove most of the felt pain.

Also worth fixing regardless

The silent discard past 8 requests per turn. Whatever the cap is, exceeding it should tell the operator that requests were dropped — a queue that quietly truncates reads as "nothing else needed approving", which is the opposite of true.

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingcluster:approvalsHuman-in-the-loop boundary, policy, and approval surfacespriority: p2Soon. Real but survivable — a rough edge, a gap, a thing that will bite later.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions