Skip to content

fix(code): release the turn when an interrupted worker never starts - #5196

Merged
Mason Daugherty (mdrxy) merged 4 commits into
mainfrom
mdrxy/code/interrupt-queue-wedge
Jul 30, 2026
Merged

fix(code): release the turn when an interrupted worker never starts#5196
Mason Daugherty (mdrxy) merged 4 commits into
mainfrom
mdrxy/code/interrupt-queue-wedge

Conversation

@mdrxy

@mdrxy Mason Daugherty (mdrxy) commented Jul 30, 2026

Copy link
Copy Markdown
Member

Pressing Esc to interrupt the agent could leave the session permanently stuck, unable to accept any new work.

What users saw

You interrupt a turn, then try to send a follow-up message. Instead of going to the agent, it shows up dimmed — queued behind a turn that already ended and will never finish. From there:

  • Esc just pops the text back into the input box ("Queued message moved to input"), and re-submitting queues it again.
  • /force-clear doesn't help.
  • The only way out is relaunching the TUI.

Why it happened

The session tracks whether the agent is busy with a single flag. While that flag is set, typed messages are queued instead of sent. Only one piece of code clears it: the cleanup that runs when a turn finishes.

The bug is that a turn can end without ever reaching its cleanup. Three ways that happened:

  • The turn was cancelled before it started running. Textual marks a background worker as RUNNING the moment it's created, before its code actually begins. Interrupting inside that window throws the whole turn away without running any of it — including the cleanup — while the worker still looks alive from the outside. Later interrupts just re-cancel something already dead.
  • Turn setup ran outside the block cleanup is attached to. _run_agent_task had two early-return guard clauses, and it persisted goal/rubric state before entering its try. A rejected turn, or an interrupt landing during that setup, skipped cleanup entirely.
  • The busy flag was set before a worker existed. _send_to_agent sets it first so the UI reacts immediately, then does some awaited setup before starting the worker. Bailing out during that setup left the session busy with no worker at all.

The fix

Make it explicit who is responsible for releasing the flag in each case:

  • Turn setup moves inside the try whose finally does the cleanup, so any early exit still releases the turn.
  • _send_to_agent releases the flag itself when it never got as far as starting a worker.
  • A new _agent_turn_started marker lets the interrupt paths tell "cancelled before it ran" apart from a live turn, and release it directly.

A message queued behind an abandoned turn is now drained rather than stranded.

Made by Open SWE

An interrupt can leave the session permanently busy: every later message is
queued instead of sent, Esc only pops it back into the chat input, and even
`/force-clear` cannot recover. Three paths could strand `_agent_running`:
a worker cancelled before its first event-loop step never runs its coroutine
(so `_run_agent_task`'s `finally` never calls `_cleanup_agent_task`), the
early returns and awaited setup in `_run_agent_task` sat outside that
`try/finally`, and `_send_to_agent` reports busy before a worker exists to
cancel.

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
@github-actions github-actions Bot added dcode Related to `deepagents-code` fix A bug fix (PATCH) internal User is a member of the `langchain-ai` GitHub organization size: M 200-499 LOC labels Jul 30, 2026
@mdrxy
Mason Daugherty (mdrxy) marked this pull request as ready for review July 30, 2026 18:04
Mason Daugherty (mdrxy) and others added 2 commits July 30, 2026 14:04
Tie `_agent_turn_started` to the worker's lifetime by clearing it in
`_cleanup_agent_task`. It was only ever reset by `_send_to_agent`, so a
completed turn left it set and `_recover_unstarted_agent_worker` bailed
for every later turn — the wedge returning one turn on. Its docstring
now describes the real lifetime, including the second entry point
(`_run_goal_criteria_request`) that runs the coroutine with no worker.

Separate setup faults from agent failures. Moving turn setup inside the
stream's `try` put local TUI and state errors in reach of the handler
that renders `Agent error: ...`, so a widget bug read as a model or
backend failure and triggered credential-mismatch detection that cannot
apply. A `streaming_started` marker now routes those to a distinct
internal-error message.

Also: the missing-`_ui_adapter` branch reports instead of returning
silently; `_release_unstarted_turn` clears the active-message state,
tolerates a torn-down DOM, and surfaces a drain failure the user would
otherwise never see; `_release` no longer lets a cleanup failure exit
the app through Textual's exception handler; the plugin-reload path
releases via `_set_agent_running` so the quiescence event follows;
`criteria_request_id` is read before the guard clauses so an early
return clears the submitted request.

Comments claiming a sole caller or sole release path were false — one
falsified by a method added alongside them — and the same wedge
narrative was restated at six sites for three different failure modes.
The attribute docstring is now the single canonical explanation.

Tests: cover later-turn recovery, `/force-clear`, `/restart` with an
unstarted worker, the stale-worker re-check, and setup-error labeling.
The queue-drain test previously passed whether the message was
delivered or dropped; it now fails only the abandoned turn's setup and
asserts delivery. The interrupt test asserts its never-stepped
precondition rather than relying on a comment.
@github-actions github-actions Bot added size: L 500-999 LOC and removed size: M 200-499 LOC labels Jul 30, 2026
@mdrxy
Mason Daugherty (mdrxy) merged commit 42e01a9 into main Jul 30, 2026
96 of 97 checks passed
@mdrxy
Mason Daugherty (mdrxy) deleted the mdrxy/code/interrupt-queue-wedge branch July 30, 2026 19:21
Mason Daugherty (mdrxy) pushed a commit that referenced this pull request Jul 31, 2026
> [!CAUTION]
> Merging this PR will automatically publish to **PyPI** and create a
**GitHub release**.

For the full release process, see
[`.github/RELEASING.md`](https://github.com/langchain-ai/deepagents/blob/main/.github/RELEASING.md).

---

_Release notes preview: keep this section in sync with the package
`CHANGELOG.md`. Publish reads the merged CHANGELOG via `release.yml`,
not this PR description — keep them aligned anyway so the PR stays an
accurate historical record for reviewers and anyone returning later._

---


##
[0.1.51](deepagents-code==0.1.50...deepagents-code==0.1.51)
(2026-07-31)

### Features

- The status bar and usage view now show the running session cost.
([#5036](#5036))
- Removed redundant `shell` and `web_search` prompt guidance.
([#5213](#5213))
- After switching threads, Deep Agents now points back to the previous
thread.
([#5172](#5172))
- Leaving `/mcp` with pending toggles now prompts you to reconnect.
([#5211](#5211))
- `dcode config get` now accepts configuration sections.
([#5134](#5134))

### Fixes

- Kept the `/goal` criteria prompt responsive.
([#5142](#5142))
- Improved goal handling so underspecified objectives can be resolved
from conversation context.
([#5201](#5201))
- Released the turn when an interrupted worker never starts.
([#5196](#5196))
- Hid timestamp footers together with their associated rows.
([#5167](#5167))
- Fixed editable SDK detection by scanning and correlating SDK locations
more accurately.
([#5199](#5199))
- Improved `doctor` output to explain why it may not have a
latest-version answer.
([#5209](#5209))

_End release notes preview._

---

> [!NOTE]
> A **New Contributors** section is appended to the GitHub release notes
automatically at publish time (see [Release
Pipeline](https://github.com/langchain-ai/deepagents/blob/main/.github/RELEASING.md#release-pipeline),
step 2).

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: langchain-oss-automated-triage[bot] <248757908+langchain-oss-automated-triage[bot]@users.noreply.github.com>
Mason Daugherty (mdrxy) added a commit that referenced this pull request Aug 4, 2026
Interrupting a turn, or exiting while a background thread-cache refresh
is in flight, no longer strands an unawaited coroutine or an open SQLite
handle.

---

The unit suite emitted three families of warnings, all at teardown. Two
are ours and are fixed here; the third is upstream and is filtered.

### An unawaited turn coroutine

`_send_to_agent` handed `run_worker` an already-built `_run_agent_task`
coroutine. Textual never runs the work of a worker cancelled before its
first event-loop step, so that coroutine was finalized unawaited — a
`RuntimeWarning`, and (once interpreter teardown had gone far enough to
break the import machinery its cleanup relies on) an unraisable
`KeyError: '__import__'`. Passing a callable means the coroutine only
exists if the worker actually runs, so there is nothing to strand.
Several tests were closing that coroutine by hand purely to silence the
warning; those workarounds are gone, and a new test pins the callable
contract.

**Where it started:** #5196. Handing `run_worker` a coroutine dates all
the way back to the original Textual REPL (#686), but it was harmless
until something cancelled a worker before its first step. #5196 added
the recovery path for exactly that situation along with the tests that
exercise it. Its parent commit runs the app test module with zero `never
awaited` warnings; #5196 itself produces three.

### An unclosed SQLite handle

`aiosqlite` opens the database on its worker thread and hands the raw
`sqlite3.Connection` back through a future, recording it on the
connection only when the awaiting coroutine resumes. A cancel landing
anywhere in that window left the handle unreachable from the cleanup
that follows, so the garbage collector reported `ResourceWarning:
unclosed database`. There are two halves to the window, and both are now
covered:

- Cancelled while the worker is still opening, the library has no handle
recorded yet, so the cleanup it queues closes nothing. The session
module now records the handle from the worker thread the moment the
connector returns.
- Cancelled after the handle is delivered but before the coroutine
resumes, the library clears its own reference before that queued cleanup
can run — so it again closes nothing. The guard now also queues an
explicit close ahead of the library's cleanup, while the handle is still
reachable.

Both closes run on the thread that opened the handle, and closing twice
is a no-op, so neither disturbs a normal shutdown. `get_checkpointer`
builds its connection through the same helper rather than
`AsyncSqliteSaver.from_conn_string`, so it gets the same guard.

**Where it started:** #5174. The prewarm that reads the session database
has existed since #1481, but it ran once at startup, so it had normally
finished before anything cancelled it. #5174 re-fires it after every
turn, which reliably leaves a session-DB read in flight when a test app
exits. Counting handles that `aiosqlite` opened and never closed across
the goal-command tests: zero on the parent commit, fourteen on #5174.

### A `typing` deprecation from `google-genai`

`google.genai.types` builds a union alias out of
`typing._UnionGenericAlias`, which CPython 3.14 deprecates, and it fires
at import before any of this package's code runs. This one is not a
regression from any change here — `deepagents-code` has been tested on
the 3.14 leg since the package was created in #3027, and the warning
appears wherever a test imports the Google integration. It is tracked
upstream as
[googleapis/python-genai#1640](googleapis/python-genai#1640)
and still unfixed as of `google-genai` 2.13.0, so it is filtered
narrowly (message, category, and module) rather than worked around.

Made by [Open
SWE](https://openswe.vercel.app/agents/f42590ef-0fe4-0b9f-6880-1c48b28446d5)

---------

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dcode Related to `deepagents-code` fix A bug fix (PATCH) internal User is a member of the `langchain-ai` GitHub organization size: L 500-999 LOC

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant