Skip to content

perf(code): refresh /threads cache after each turn - #5174

Merged
Mason Daugherty (mdrxy) merged 2 commits into
mainfrom
mdrxy/code/threads-cache-refresh
Jul 30, 2026
Merged

perf(code): refresh /threads cache after each turn#5174
Mason Daugherty (mdrxy) merged 2 commits into
mainfrom
mdrxy/code/threads-cache-refresh

Conversation

@mdrxy

Copy link
Copy Markdown
Member

A thread you just started now shows up in the /threads switcher as soon as you open it, instead of appearing a moment later.


The thread picker paints instantly from an in-memory cache of recent threads and only re-queries the session database afterwards. That cache was populated exactly once, by a startup worker, so any thread created during the session was absent from the first paint: the new row only appeared after the modal's list query, its configured-agent filesystem scan, and a full teardown/rebuild of every row widget had completed. On a large sessions.db the list query alone measured ~0.2 s warm (~0.8 s on the first call of a process, which also builds the covering index), which is exactly the window where the user sees a list that is missing the thread they just used.

Rather than trying to make that refresh path faster, this keeps the cache honest: _cleanup_agent_task — the turn-end hook that already refreshes the git branch — now schedules the same prewarm the app runs at startup, in an exclusive worker group so overlapping turns coalesce. Turn end is the right moment because the turn's checkpoints have just been written and the app is otherwise idle, and it is cheap to repeat: the per-thread message-count and initial-prompt caches are keyed on checkpoint freshness, so only the thread that actually changed is read back from disk. With the cache current, the picker's first paint already contains the new thread (with its updated timestamp and message count), and the follow-up query finds nothing to change, so the row rebuild is skipped entirely.

Deletion already pruned this cache in place, so this fills in the other half of keeping it in sync with in-session activity. Scheduling is skipped while the app is exiting or not running, mirroring the existing skill-rediscovery guard.

Made by Open SWE

The `/threads` picker paints from an in-memory cache that was only filled
once at startup, so a thread created mid-session was missing from the
first paint until the modal's database query, agent scan, and row rebuild
finished. Refreshing the cache at turn end removes that wait.

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
@github-actions github-actions Bot added dcode Related to `deepagents-code` internal User is a member of the `langchain-ai` GitHub organization performance Code change that improves performance size: S 50-199 LOC labels Jul 29, 2026
@mdrxy
Mason Daugherty (mdrxy) marked this pull request as ready for review July 30, 2026 14:54
@mdrxy
Mason Daugherty (mdrxy) merged commit 872f9d5 into main Jul 30, 2026
54 checks passed
@mdrxy
Mason Daugherty (mdrxy) deleted the mdrxy/code/threads-cache-refresh branch July 30, 2026 14:58
Mason Daugherty (mdrxy) pushed a commit that referenced this pull request Jul 30, 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.50](deepagents-code==0.1.49...deepagents-code==0.1.50)
(2026-07-30)

### Highlights

- Added project hooks workspace trust and expanded Hooks v2 support with
client and server lifecycle events plus runtime feedback
([#5105](#5105),
[#5104](#5104),
[#4997](#4997),
[#5045](#5045)).
- Added an option to mute the “YOLO is active” toast
([#5103](#5103)).
- Made the splash screen `thread` ID clickable to copy it
([#5173](#5173)).
- Show `ask_user` answers directly on the answered tool row
([#5100](#5100)).
- Show a toast when submitting an empty required `ask_user` answer
([#5095](#5095)).
- Added thread message counts to the Debug Console
([#5117](#5117)).

### Fixes and improvements

- Gated Hooks v2 behind `DEEPAGENTS_CODE_EXPERIMENTAL` and improved hook
resume stability across identity and Command tool results
([#5146](#5146),
[#5176](#5176)).
- Kept server hook state out of task results
([#5164](#5164)).
- Stopped duplicate Auto transcript events during interrupt replay
([#5157](#5157)).
- Kept `/update` and `/install --package` prompts responsive
([#5127](#5127)).
- Refreshed the `/threads` cache after each turn
([#5174](#5174)).
- Anchored toasts above the chat input and added a toast when media is
dropped into a free-text question
([#5101](#5101),
[#5099](#5099)).
- Improved thread status message styling and links
([#5118](#5118)).
- Made resume hints echo the launched command name
([#5119](#5119)).
- Scoped selection copy to the clicked screen
([#5140](#5140)).
- Ignored mouse hits on detached widgets
([#5114](#5114)).

_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>
Co-authored-by: Johannes du Plessis <johannes@langchain.dev>
Mason Daugherty (mdrxy) added a commit that referenced this pull request Jul 30, 2026
A thread you just started now shows up in the `/threads` switcher as
soon as you open it, instead of appearing a moment later.

---

The thread picker paints instantly from an in-memory cache of recent
threads and only re-queries the session database afterwards. That cache
was populated exactly once, by a startup worker, so any thread created
during the session was absent from the first paint: the new row only
appeared after the modal's list query, its configured-agent filesystem
scan, and a full teardown/rebuild of every row widget had completed. On
a large `sessions.db` the list query alone measured ~0.2 s warm (~0.8 s
on the first call of a process, which also builds the covering index),
which is exactly the window where the user sees a list that is missing
the thread they just used.

Rather than trying to make that refresh path faster, this keeps the
cache honest: `_cleanup_agent_task` — the turn-end hook that already
refreshes the git branch — now schedules the same prewarm the app runs
at startup, in an exclusive worker group so overlapping turns coalesce.
Turn end is the right moment because the turn's checkpoints have just
been written and the app is otherwise idle, and it is cheap to repeat:
the per-thread message-count and initial-prompt caches are keyed on
checkpoint freshness, so only the thread that actually changed is read
back from disk. With the cache current, the picker's first paint already
contains the new thread (with its updated timestamp and message count),
and the follow-up query finds nothing to change, so the row rebuild is
skipped entirely.

Deletion already pruned this cache in place, so this fills in the other
half of keeping it in sync with in-session activity. Scheduling is
skipped while the app is exiting or not running, mirroring the existing
skill-rediscovery guard.

Made by [Open
SWE](https://openswe.vercel.app/agents/35c3b07d-9fe1-e077-4cdf-e6b7854b4f18)

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
Mason Daugherty (mdrxy) pushed a commit that referenced this pull request Jul 30, 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.50](deepagents-code==0.1.49...deepagents-code==0.1.50)
(2026-07-30)

### Highlights

- Added project hooks workspace trust and expanded Hooks v2 support with
client and server lifecycle events plus runtime feedback
([#5105](#5105),
[#5104](#5104),
[#4997](#4997),
[#5045](#5045)).
- Added an option to mute the “YOLO is active” toast
([#5103](#5103)).
- Made the splash screen `thread` ID clickable to copy it
([#5173](#5173)).
- Show `ask_user` answers directly on the answered tool row
([#5100](#5100)).
- Show a toast when submitting an empty required `ask_user` answer
([#5095](#5095)).
- Added thread message counts to the Debug Console
([#5117](#5117)).

### Fixes and improvements

- Gated Hooks v2 behind `DEEPAGENTS_CODE_EXPERIMENTAL` and improved hook
resume stability across identity and Command tool results
([#5146](#5146),
[#5176](#5176)).
- Kept server hook state out of task results
([#5164](#5164)).
- Stopped duplicate Auto transcript events during interrupt replay
([#5157](#5157)).
- Kept `/update` and `/install --package` prompts responsive
([#5127](#5127)).
- Refreshed the `/threads` cache after each turn
([#5174](#5174)).
- Anchored toasts above the chat input and added a toast when media is
dropped into a free-text question
([#5101](#5101),
[#5099](#5099)).
- Improved thread status message styling and links
([#5118](#5118)).
- Made resume hints echo the launched command name
([#5119](#5119)).
- Scoped selection copy to the clicked screen
([#5140](#5140)).
- Ignored mouse hits on detached widgets
([#5114](#5114)).

_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>
Co-authored-by: Johannes du Plessis <johannes@langchain.dev>
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` internal User is a member of the `langchain-ai` GitHub organization performance Code change that improves performance size: S 50-199 LOC

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant