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 — dropView on GitHub (pinned to a1af029e6e)
Solutions
- 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.
- Inspect `aget_state(config)` — see whether `next`/`tasks`/`interrupts` remain and which one keeps the thread pending.
- Retry the recovery on the same thread; a transient 409 or cancelled-wait timeout can leave work behind on the first attempt.
- 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
- Confirm cancellations actually took effect in server logs before abandoning work.
- Resolve outstanding interrupts before attempting to abandon a thread.
- After any recovery, snapshot the state and assert `next`/`tasks`/`interrupts` are empty.
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
- Server process exited with code {self._process.returncode}
- Server graph '{graph_name}' failed readiness check (status:
- thread_id is required in config.configurable
- `expires_at` is only meaningful when `logged_in` is True.
- `is_expired` is only meaningful when `logged_in` is True.
AI-assisted analysis of langchain-ai/deepagents@a1af029e6e (2026-08-29).
Data as JSON: /api/errors/fbb392e0cd4d0a93.
Report an issue: GitHub.