# Approval binding — dispatch, persistence and retry evidence

**Research ID K06 extension · Checked September 21, 2026 · Primary-source static inspection.** Supports [What exactly did you approve?](/blog/what-exactly-did-you-approve). Extends the original K06 implementation brief, whose locator table is folded into AB1 below. At the September check no upstream tests, model requests, attacks or applications were executed; on October 22, 2026 the pinned middleware test file was run once locally (see AB3).

**Public copy prepared 2026-10-22.** Pins re-checked: the four pinned LangChain files (AB1–AB4) were re-fetched from the pinned revision and their SHA-256 digests match the September record; every function, test name and the `#L1688` line anchor cited by the article resolve at that revision; the two PyPI wheels (AB5, AB6) were re-downloaded and their SHA-256 digests match; the pinned middleware test file was run once in an isolated environment (53 passed, 0 failed). Not re-checked: the mutable maintainer documentation pages (AB7, AB8) and the PyPI upload timestamps, which were carried from the September check.

## Scope and conclusion

The full manuscript is justified as a **bounded implementation analysis**. Its trace reaches the framework's tool invocation boundary. The broader claim “the human approved the actual external effect” remains unestablished without an application, authenticated reviewer interface, persistent backend and external sender.

The original gate is resolved in parts:

| Gate | Evidence acquired | Disposition |
|---|---|---|
| Edited call to dispatch | Same LangChain SHA, factory composition, lock-selected ToolNode implementation | Statically traced to `tool.invoke` / `tool.ainvoke`; upstream fake-tool tests pass locally (2026-10-22); no application runtime confirmation |
| Rejection | Factory pending-call filter plus upstream fake-tool test body | Explains why retaining proposal does not dispatch rejected call |
| Persistence | Middleware state update, factory checkpointer forwarding, versioned interrupt function and maintainer docs | Framework mechanisms described; restart safety of a real backend **unresolved** |
| Retry | Lock-selected graph retry function, wrapper composition and interrupt docs | Conditional re-invocation distinguished from interrupt/resume; external-effect deduplication **unresolved** |
| Preview to external effect | No application selected or deployed | Explicitly outside article's claim; future mock exercise needed |

## Source register

All sources below are **primary**, authored by the respective **LangChain/LangGraph contributors or maintainers**. Code is evidence of implementation text, tests are specifications (read in September, run once in October in the framework's own fixtures), and mutable documentation is product guidance. None is independent deployment validation.

### AB1 — Approval middleware

- **Source:** LangChain contributors, `human_in_the_loop.py`; commit `4af7ab8fcb98c2dc211d9e34cddedffa23eba4d9`, committer timestamp **2026-09-20T18:52:00Z** (carried from K06), checked **2026-09-21**, re-fetched **2026-10-22** (digest unchanged).
- **URL:** [Pinned implementation](https://github.com/langchain-ai/langchain/blob/4af7ab8fcb98c2dc211d9e34cddedffa23eba4d9/libs/langchain_v1/langchain/agents/middleware/human_in_the_loop.py).
- **Observed / locators:** `_process_decision`, `after_model`, `_reviewer_edit`, `_apply_edit`, `wrap_tool_call`, `awrap_tool_call`; original proposal remains while replacements are stored by call ID under the private state key `hitl_edited_tool_calls` and applied to the handler request. Rejection produces an error tool message; `respond` produces a success message.

| Locator | Direct static observation |
|---|---|
| `_create_action_and_config` | Builds review request with tool name, args and description |
| `after_model` | Batches selected calls, checks decision count and applies decisions by interrupted-call order |
| `_process_decision` | Approve retains the call; edit also retains it in message history |
| `after_model` / `_reviewer_edit` | Records reviewer replacements by tool-call ID; retrieves them for execution |
| `_apply_edit` | Overrides tool name/args; resolves a changed tool name against available tools and raises if absent |
| `wrap_tool_call` / `awrap_tool_call` | Applies replacement before passing request to handler; annotates result |
| `after_model`, no-interrupt path | Clears prior edits when an ensuing turn has no review actions |

- **Inference:** an activity view needs to distinguish proposal from authorized replacement. No inference of an approval bypass.
- **Confidence / limit:** high for selected static path; no reviewer authentication, complete middleware stack or external sender audit.
- **Decision / refresh:** supports article lead; compare implementation and UI behavior when adopting another release or constructing an integration.
- **SHA-256:** `e7143b5022639c244ab20222229fe45e6d0ae48338f15439971cad31b0608f31`.

### AB2 — Agent factory

- **Source:** LangChain contributors, `factory.py`, same commit/date; checked **2026-09-21**, re-fetched **2026-10-22** (digest unchanged).
- **URL:** [Pinned factory](https://github.com/langchain-ai/langchain/blob/4af7ab8fcb98c2dc211d9e34cddedffa23eba4d9/libs/langchain_v1/langchain/agents/factory.py).
- **Observed / locators:** `_chain_tool_call_wrappers` composes first wrapper outermost; `create_agent` passes composed wrappers into ToolNode and forwards `checkpointer` into graph compilation. `_make_model_to_tools_edge` excludes calls with matching tool messages from pending dispatch.
- **Inference:** wrapper ordering is part of the application review, and a transcript's retained rejected proposal does not alone imply execution.
- **Confidence / limit:** high for static functions; arbitrary additional middleware can change requests and behavior.
- **Decision / refresh:** resolves dispatch/rejection source gate; revisit on factory changes or extra middleware.
- **SHA-256:** `ecbb580ed4083a985a40c71735aa0eafb58d3a00ac74d2e511ab14a3aa3d37b1`.

### AB3 — Upstream middleware tests

- **Source:** LangChain contributors, `test_human_in_the_loop.py`, same commit/date; checked **2026-09-21**, re-fetched and run **2026-10-22** (digest unchanged).
- **URL:** [Pinned tests](https://github.com/langchain-ai/langchain/blob/4af7ab8fcb98c2dc211d9e34cddedffa23eba4d9/libs/langchain_v1/tests/unit_tests/agents/middleware/implementations/test_human_in_the_loop.py).
- **Observed:** read bodies of `test_human_in_the_loop_middleware_rejected_call_not_executed_and_stays_paired` (line 250), `test_human_in_the_loop_middleware_edit_executes_reviewers_call` (line 1401), and `test_recorded_edit_survives_a_later_tightening_of_allowed_decisions` (line 1688). Assertions cover zero rejected-tool invocations, corrected arguments with retained original proposal, and preservation of a recorded send→draft replacement under changed allowed-decision configuration.
- **Run record (2026-10-22):** the repository archive for commit `4af7ab8fcb98c2dc211d9e34cddedffa23eba4d9` was downloaded into a temporary directory outside this repository; in `libs/langchain_v1`, `uv sync --frozen --group test` (uv 0.12.0, Python 3.13.14, lock-resolved `langgraph 1.2.11`, `langgraph-prebuilt 1.1.0`, `langgraph-checkpoint 4.2.0`, `pytest 9.1.1`), then `uv run --frozen --no-sync pytest tests/unit_tests/agents/middleware/implementations/test_human_in_the_loop.py -q`. Result: **53 passed, 0 failed, 0 errors** in 0.55 s. The three tests named above are among the 53. Network access was limited to package installation; no model requests were made.
- **Rerun and dispatch capture (2026-10-23):** the same file was run again (53 passed), and a scripted approve/edit/reject capture observed from outside the model that the tool received the replacement, history kept the proposal and the rejected call never reached the tool. See [I05 investigation record](/research/langchain-hitl-dispatch.md).
- **Inference:** preserving a correction and revoking authorization are separate requirements.
- **Confidence / limit:** high as specification and as a one-off local pass. The tests use fake models, fake tools and in-memory checkpoints; the third constructs a request directly. None demonstrates process-restart persistence, an authenticated reviewer or an external send, and a pass in the framework's fixtures says nothing about a reader's application.
- **Decision / refresh:** prevents mischaracterizing intended correction behavior as a vulnerability; revisit with changed tests or an application-level reproduction.
- **SHA-256:** `7f1bb0f8cb14494ca7732e071a265906ee2c42d7118a4a57a68ce68f76c7166d`.

### AB4 — Lockfile dependency selection

- **Source:** LangChain contributors, `libs/langchain_v1/uv.lock`, same commit/date; checked **2026-09-21**, re-fetched **2026-10-22** (digest unchanged).
- **URL:** [Pinned lockfile](https://github.com/langchain-ai/langchain/blob/4af7ab8fcb98c2dc211d9e34cddedffa23eba4d9/libs/langchain_v1/uv.lock).
- **Observed:** selects `langgraph 1.2.11`, `langgraph-prebuilt 1.1.0`, `langgraph-checkpoint 4.2.0`.
- **Inference:** these are a reproducible basis for additional source reading; they are not asserted to be the user's installed versions or universal minimum requirements.
- **Confidence / limit:** high for this lockfile; the October test run resolved the same three versions from it.
- **Decision / refresh:** pins AB5/AB6; revisit if switching checkout or dependency resolution.
- **SHA-256:** `ee5d424e27e3bdca5dbb5881cd6759ae25462a2d5f519f1fbedef16e7fa02799`.

### AB5 — ToolNode dispatch artifact

- **Source:** LangGraph contributors, `langgraph-prebuilt 1.1.0`; selected wheel uploaded **2026-05-12T03:37:48.007261Z** according to [PyPI JSON](https://pypi.org/pypi/langgraph-prebuilt/1.1.0/json); checked **2026-09-21**; wheel re-downloaded **2026-10-22** (digest unchanged).
- **URLs:** [Release files](https://pypi.org/project/langgraph-prebuilt/1.1.0/#files), [exact wheel](https://files.pythonhosted.org/packages/e9/43/3fe1a700b8490ed02679cdbbc8c915eb23a092faf496c9c1118abcd10be3/langgraph_prebuilt-1.1.0-py3-none-any.whl).
- **Observed:** extracted `langgraph/prebuilt/tool_node.py` in memory without executing it. `_run_one` invokes the wrapper; its execute callback reaches `_execute_tool_sync`, which injects runtime arguments and calls `tool.invoke`. Async counterpart calls `tool.ainvoke`. Wrapper API permits multiple execute calls.
- **Inference:** recording only model arguments omits an important downstream boundary; external effects still depend on the custom tool.
- **Confidence / limit:** high for file text, wheel SHA matches PyPI metadata. No custom tool inspected; the October test run exercised this package only through the framework's fake tools.
- **Decision / refresh:** ends static trace at invocation, not recipient receipt; revisit for changed package or tool adapter.
- **Wheel SHA-256:** `51e311747d755b751d5c6b39b0c1446124d3a7643d2515017e6714b323508fc9`.
- **Extracted file SHA-256:** `af6df67779000d219b2e1799401238f3618511658b26316a70a5e432891040d3`.

### AB6 — Interrupt and retry artifacts

- **Source:** LangGraph contributors, `langgraph 1.2.11`; selected wheel uploaded **2026-08-11T14:00:35.494718Z** according to [PyPI JSON](https://pypi.org/pypi/langgraph/1.2.11/json); checked **2026-09-21**; wheel re-downloaded **2026-10-22** (digest unchanged).
- **URLs:** [Release files](https://pypi.org/project/langgraph/1.2.11/#files), [exact wheel](https://files.pythonhosted.org/packages/0a/7f/c5c30e4be99ff821029c7ac872a480676bb179c9f3df85ea3f38d13f86d4/langgraph-1.2.11-py3-none-any.whl).
- **Observed:** `types.py:interrupt` uses per-task interrupt indices/resume values, writes resume information and raises `GraphInterrupt` when unresolved. `pregel/_retry.py:run_with_retry` re-invokes a task on matching retry policies within limits, clears task writes before attempts, and propagates `GraphBubbleUp`. Neither inspected function supplies application sender deduplication.
- **Inference:** preserved graph state cannot establish exactly one external effect; application recovery must account for uncertain acceptance.
- **Confidence / limit:** high for narrow static mechanisms. We did not inspect every runtime path, backend storage transaction, async cancellation path, or network tool's retries. No claim that a default agent retries all failed calls.
- **Decision / refresh:** supports distinct interrupt/resume/retry vocabulary; revisit after runtime/configuration changes or a failure-recovery experiment.
- **Wheel SHA-256:** `8bab70de7b2d00b5300fb289bcf38d8b241400f3184c1e95e8ce706fb0e8686b`.
- **Extracted `types.py` SHA-256:** `7204b467ed7e724ec16bbd2e3e7fac1c698742b12dda27cf2086f744128b1844`.
- **Extracted `_retry.py` SHA-256:** `667a74090e23fda724e74ad377586e32325510cdd9042d320d60306f783f0a9d`.

### AB7 — Human-in-the-loop documentation

- **Source:** LangChain maintainers, *Human-in-the-loop*. No publication/update date displayed; checked **2026-09-21**; not re-checked for the public copy.
- **URL:** [Official documentation](https://docs.langchain.com/oss/python/langchain/human-in-the-loop).
- **Observed:** explains approve/edit/reject/respond, ordered decisions, checkpoint requirement and caveats about substantial edits producing unexpected follow-up actions.
- **Inference:** documentation is a starting interface contract, not an application threat model.
- **Confidence / limit:** high for page contents as of September; mutable and may describe newer behavior than a pinned checkout.
- **Decision / refresh:** supports review vocabulary; refresh before API examples.

### AB8 — Interrupt and persistence documentation

- **Source:** LangChain maintainers, *Interrupts* and *Persistence*. No publication/update dates displayed; checked **2026-09-21**; not re-checked for the public copy.
- **URLs:** [Interrupts](https://docs.langchain.com/oss/python/langgraph/interrupts), [Persistence](https://docs.langchain.com/oss/python/langgraph/persistence).
- **Observed:** interrupt guide describes node re-execution and idempotency concerns for pre-interrupt effects. Persistence distinguishes thread checkpointers from stores and states that in-memory state is lost at restart. The old `/durable-execution` URL redirected to persistence during the September check; no separate guide is claimed inspected.
- **Inference:** durable authorization, state recovery and external-effect deduplication need separate evidence.
- **Confidence / limit:** high for stated guidance as of September; backend recovery and authorizer configuration remain untested.
- **Decision / refresh:** narrows persistence claims and acceptance cases; refresh before platform-specific guidance.

## Original design analysis and assumptions

The example uses a fictional SDK migration note and reserved `.invalid` addresses. It assumes an application can expose the reviewed artifact and can constrain the sender to that immutable version. The digest, principal, expiry/revocation rules and external operation identifier are **proposed application controls**, not LangChain features established by these sources.

A content hash detects byte differences; it does not establish semantic safety or reviewer authority. A local operation ID cannot deduplicate a remote API unless the integration and receiving service implement suitable semantics. Stronger controls may be disproportionate for a reversible local draft.

No current LangChain defect, successful attack, production outage or customer harm is asserted. Tests in the [action record](/resources/action-approval-record.md) are **unexecuted** and result fields stay empty.

## Exact remaining evidence to collect

1. Select one intended integration and freeze its package/configuration versions.
2. Capture proposal, actual reviewer display, authenticated decision, final payload and receipt with a fake sender first.
3. Exercise changed bytes, changed destination, rejection, stale authority and cross-thread decisions.
4. Add a real persistent backend only in an independently authorized local exercise; test restart at defined boundaries.
5. Simulate accepted-but-response-lost behavior; establish the receiving API's actual reconciliation/deduplication semantics before enabling retries.
6. Measure reviewer time and legitimate completion alongside prevented changes. Do not infer usability from the control's presence.

Source files were read through raw GitHub and hash-verified PyPI wheel archives. In September none were installed or executed. On October 22, 2026 the pinned checkout was unpacked into a temporary directory outside this repository and its test dependency group installed in an isolated virtual environment to run one test file; nothing was copied into this repository. The repository contains our analysis and links, not vendored third-party implementation.
