# ApprovalNotFoundError

`runtime.approve(approval_id)` was called with an id that doesn't match any pending approval in this runtime. The framework refuses to no-op an unknown id — that would silently corrupt the approval audit trail.

Multi-inherits `LookupError` for back-compat: code that does `except LookupError` around the approval API continues to work.

## Quick fix

List currently-pending approvals and confirm the id matches:

```python
for pa in rt.pending_approvals():
    print(pa.id, pa.kind, pa.object_type, pa.reason)
```

The id you pass to `rt.approve(...)` has to be exactly one of those ids. The most common causes when it isn't:

- **The id was already approved or denied.** Approval is one-shot — each id is consumed when `approve()` succeeds or `reject()` denies it. Re-using an id after the first call always fires this error.
- **The runtime instance is fresh.** Approvals don't carry across `Runtime.load`. If you saved a run, restarted, and tried to approve an id from before the restart, the new runtime has no record of it.
- **Typo in the id.** The error message names the id you passed; the context dict also names how many approvals are currently pending, so a "currently 2 pending; none match" message is the operator signal that you have the right runtime but the wrong id.

The diligence demo's `step_2_approval_demo` (in `examples/diligence_real_run.py`) shows the canonical pattern: enumerate, then approve by id.

## How to diagnose

The error message names both the offending id and the pending count:

```text
ApprovalNotFoundError: no pending approval named 'approval_999'

What failed:
  runtime.approve('approval_999') could not find a pending approval
  with that id.
    There are currently 2 pending approvals; none of them match
    'approval_999'.
```

From code:

```python
try:
    rt.approve(approval_id)
except ApprovalNotFoundError as e:
    print(e.approval_id)
    print(e.pending_count)
```

If `pending_count` is `0` and you expected approvals, either no behavior has called `ctx.propose_object` under a gating policy yet, or the runtime is fresh after a `Runtime.load` and pending approvals weren't preserved.

## When does this fire

Only at `runtime.approve()`. The check happens twice in the implementation — once when no pack state exists (so no approvals can exist), and once after walking the pending-approvals list without finding a match. Either path produces the same error with appropriate `pending_count` context.

## Why the framework refuses to continue

Approval ids are generated by the runtime when a behavior calls `ctx.propose_object` under a gating policy (`memo_approval`, `risk_approval`, etc.). Each id is unique to its runtime instance and is consumed when `approve()` succeeds. A miss could mean the id is stale, the runtime is fresh, or the id is a typo. The framework refuses to no-op rather than guess — silently doing nothing on an unknown id would mean an operator could approve an already-applied or non-existent action without noticing the call didn't do anything, and the audit trail would lie about what was approved.

See [`failure-model`](https://docs.activegraph.ai/concepts/failure-model/index.md) for the broader principle.

## What's related

- [`concepts/policies`](https://docs.activegraph.ai/concepts/policies/index.md) — the policy mechanism that produces pending approvals. Covers the `ctx.propose_object` → `approval.proposed` → `runtime.approve()` → `approval.granted` lifecycle.
- `examples/diligence_real_run.py:step_2_approval_demo` — the canonical enumerate-then-approve pattern, runnable.
