langchain-ai/deepagents · error · RuntimeError

Pending graph work remained on thread {thread_id} after clea

Error message

Pending graph work remained on thread {thread_id} after clearing checkpoint state

What it means

`aabandon_pending_work` cancels active runs, writes terminal tool results for dangling tool calls, and sets the thread's state to `__end__` to discard checkpointed work. It then re-reads the state and, if `state_has_pending_work` still reports a queued node (`next`), task, or interrupt, raises this RuntimeError — the post-clear verification failed, so the thread was not safely cleaned up.

Source

Thrown at libs/code/deepagents_code/client/remote_client.py:753

        await _cancel_active_runs(self._get_graph(), thread_id)
        state = await self.aget_state(prepared)
        cancelled = await asyncio.to_thread(
            _cancelled_tool_messages, getattr(state, "values", None)
        )
        if cancelled:
            # `create_agent` names the tool step "tools", but only adds the node
            # when the agent has tools. A toolless graph therefore rejects this
            # update with `InvalidUpdateError` -- and could not have produced a
            # dangling tool call in the first place, so the branch is dead
            # there. The caller reports the failure rather than compacting.
            await self.aupdate_state(prepared, {"messages": cancelled}, as_node="tools")
        await self.aupdate_state(prepared, None, as_node="__end__")
        if state_has_pending_work(await self.aget_state(prepared)):
            msg = (
                f"Pending graph work remained on thread {thread_id} after "
                "clearing checkpoint state"
            )
            raise RuntimeError(msg)

    async def aput_store_item(
        self,
        namespace: tuple[str, ...],
        key: str,
        value: dict[str, Any],
    ) -> None:
        """Write an item to the server-side LangGraph Store.

        Args:
            namespace: Store namespace.
            key: Item key within `namespace`.
            value: JSON-serializable item value.

        Notes:
            A failed write is logged at debug and re-raised. The re-raise is
            load-bearing: callers (`awrite_approval_mode` and its callers)
            depend on the failure propagating so they can fail closed — drop

View on GitHub (pinned to a1af029e6e)

Solutions

  1. Check server logs to confirm the active run was actually cancelled (`_cancel_active_runs` is best-effort); re-run `acancel_active_runs` and then `aabandon_pending_work` again.
  2. Inspect `aget_state(config)` — see whether `next`/`tasks`/`interrupts` remain and which one keeps the thread pending.
  3. Retry the recovery on the same thread; a transient 409 or cancelled-wait timeout can leave work behind on the first attempt.
  4. If an interrupt persists, resume/resolve it explicitly (or clear it via `aupdate_state`) before abandoning; if the server keeps re-queuing runs, restart the server and retry.
Defensive patterns

Strategy: retry

Validate before calling

# After abandonment, verify the thread is clean yourself
snapshot = await agent.aget_state(config)
assert not (snapshot.next or snapshot.tasks or snapshot.interrupts), "thread still has pending work"

Type guard

def is_thread_clean(state: object) -> bool:
    return not (
        getattr(state, "next", None)
        or getattr(state, "tasks", None)
        or getattr(state, "interrupts", None)
    )

Try / catch

try:
    await agent.aabandon_pending_work(config)
except RuntimeError as exc:
    if "Pending graph work remained" in str(exc):
        logging.warning("Recovery verification failed; cancelling again and retrying")
        await agent.acancel_active_runs(config)
        await agent.aabandon_pending_work(config)
    else:
        raise

Prevention

When it happens

Trigger: After cancel + `aupdate_state(None, as_node='__end__')`, the fresh `StateSnapshot` still has non-empty `next`, `tasks`, or `interrupts` — e.g. the run was not actually cancelled, the state update did not land (409 retry also failed), or an interrupt was re-materialized.

Common situations: A server that ignored the cancellation and kept the run queued; the `__end__` update rejected or conflicting so the checkpoint still points at the `tools` node; leftover interrupts persisted by the checkpointer that the end-write did not clear.

Related errors


AI-assisted analysis of langchain-ai/deepagents@a1af029e6e (2026-08-29). Data as JSON: /api/errors/fbb392e0cd4d0a93. Report an issue: GitHub.