Skip to content

fix(code): avoid transcript hydration lag - #5479

Merged
Mason Daugherty (mdrxy) merged 14 commits into
mainfrom
johannes117/code/smooth-transcript-history
Aug 18, 2026
Merged

fix(code): avoid transcript hydration lag#5479
Mason Daugherty (mdrxy) merged 14 commits into
mainfrom
johannes117/code/smooth-transcript-history

Conversation

@johannes117

@johannes117 Johannes du Plessis (johannes117) commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Resumed transcripts now load quickly and scroll smoothly without blocking on message hydration.


Textual reapplies styles across the mounted DOM whenever history widgets are inserted. This made on-demand hydration stutter even in small batches, especially because collapsed tool rows still retained their full widget trees.

This mounts a small initial history window, warms it in bounded batches, prefetches relative to virtual spacer boundaries, coalesces hydration work, and delays pruning until scrolling settles. Restored successful tool runs are represented by lazy data-backed summaries whose detail widgets mount only when expanded.

On a 681-message local thread, restored history compacted to 265 display rows, initial loading took about 0.9 seconds, and background warmup completed the transcript before backward scrolling required hydration.

Test plan
  • 623 focused tests passed
  • Ruff passed on changed files
  • ty passed on changed production files

Video

New (left) - Old (right)

Virtualization.Side.by.side.mp4

@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: XL 1000+ LOC labels Aug 13, 2026

@open-swe open-swe Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Open SWE Review found 1 potential issue.

Open in WebView Open SWE trace

Comment thread libs/code/deepagents_code/tui/widgets/messages.py Outdated
@github-actions github-actions Bot added size: L 500-999 LOC and removed size: XL 1000+ LOC labels Aug 13, 2026
@github-actions github-actions Bot added size: XL 1000+ LOC and removed size: L 500-999 LOC labels Aug 13, 2026

@mdrxy Mason Daugherty (mdrxy) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you expand a saved group of tool calls, then expand one tool’s output inside it, that "inner expanded" state looks like can be lost when the UI scrolls the group out of memory.

@mdrxy

Copy link
Copy Markdown
Member

Posted by Codex

I found a few issues worth addressing before merge. I have kept these to behaviors that follow directly from the implementation; the larger hydration-oscillation claim needs a reproducible regression test before treating it as a blocker.

  1. _continue_hydration() now checks both edges after every hydration slice.

_hydrate_messages_above() schedules a below-edge prune, and _hydrate_messages_below() schedules an above-edge prune. Once the continuation runs, it calls both _check_hydration_needed() and _check_hydration_below_needed() regardless of which direction was just hydrated.

The prior implementation re-checked only the direction that had just hydrated. Please add coverage for a transcript with archived rows on both sides and verify that an idle viewport does not repeatedly request alternating hydration directions. If the symmetric re-check is intentional, document the termination condition.

  1. A permanently unbuildable edge message is retried forever.

Both hydrate loops stop on the first to_widget() failure, correctly preserving contiguity, but leave the failing entry as the next candidate. A later scroll retries the same failure indefinitely; the spacer continues to reserve estimated height for content that will never mount. _notify_hydration_failure() is also session-latched, so a subsequent distinct failure may have no visible feedback.

Please either record a recoverable failure state for that row, or provide a bounded retry/skip strategy that preserves ordering without leaving an unfillable virtual gap.

  1. Batch rollback hides removal failures.

remove_attached_nodes() suppresses every Exception from node.remove() without logging. If a partial mount fails and rollback leaves a node attached, the store remains unchanged and the next hydration can attempt to mount the same IDs again.

At minimum, log rollback failures. It would also be safer to make the batch failure explicit rather than silently relying on a future hydrate attempt to repair a partially attached DOM.

  1. Tool completion uses a stale shared prune direction.

_sync_tool_message_state() schedules pruning with _transcript_prune_direction, which represents the last hydration direction rather than the direction appropriate for this newly completed tool. For example, after hydrate-above, the stored direction is "below" and a tool completion can prune recent completed rows from the tail.

Please pass an explicit direction here, as _mount_message() does, rather than reusing the shared hydration state.

  1. Lazy-group expansion fails without user-visible recovery.

On expansion failure, LazyToolGroupSummary._set_expanded() logs, removes any partial children, and returns. The group remains collapsed and emits no ExpansionChanged event or user-facing notification. For restored transcripts, this can make the retained tool details inaccessible with no explanation.

Please notify the user on failure, or render a visible fallback/error state in the summary.

  1. The collapse branch has no equivalent exception handling.

The expansion path catches errors, but collapse performs zip(..., strict=True) and remove_children() outside that handler. A mismatch between _message_data and _detail_widgets therefore escapes the worker. The lists should be represented as one collection of paired data/widgets, or collapse should handle and report this inconsistency consistently with expansion.

  1. Pruning can stop recovering after a missing-widget desync.

When pruning cannot find a candidate widget, it logs at debug level and does not mark it pruned, which is correct. But _run_transcript_prune_slice() only schedules another slice when pruned > 0. The same missing head/tail candidate can therefore prevent all further pruning while the visible range continues to exceed the soft limit.

Please add a recovery path for this state—for example, reschedule with bounded retries or reconcile the mounted IDs against the store.

  1. Cancelled assistant rendering is treated as success.

asyncio.gather(..., return_exceptions=True) can return CancelledError, which is a BaseException, not an Exception. The isinstance(error, Exception) check misses it, so the batch may be marked hydrated even though an assistant’s post-mount content render was cancelled.

Please explicitly handle cancellation results, or re-raise cancellation rather than treating the batch as successful.

Non-blocking cleanup:

  • MessageData.__post_init__() requires non-empty TOOL_GROUP details, while to_widget() falls back to self.tool_group_messages or []. The fallback masks a violated invariant and can render an empty summary.
  • The prune-delay comments are misleading: scrolling also resets the timer, so it is not simply “seconds without hydration.”
  • The restore-path DIFF grouping branch appears unreachable today: restored diffs use diff_tool_name="edit_file", which is excluded from grouping.
  • Please add focused tests for generation changes during mount, retry behavior after build/mount failure, prune recovery after a missing widget, deferred lazy-group expansion, and both-direction hydration near virtual boundaries.

@johannes117

Copy link
Copy Markdown
Contributor Author

Addressed in c8f85a1:

  • preserved nested tool-output expansion state across virtualization
  • limited continuation checks to the hydration edge that just completed
  • replaced unbuildable rows with visible placeholders instead of blocking history
  • logged rollback cleanup failures and rolled back/re-raised cancelled rendering
  • surfaced lazy-group expansion/collapse failures and made collapse state tracking consistent
  • reconciled missing boundary widgets during pruning
  • tightened the tool-group invariant and removed stale prune wording/dead restore grouping

The shared prune direction remains unchanged because it tracks the edge to trim for the current mounted window.

@mdrxy
Mason Daugherty (mdrxy) merged commit 3c8cae6 into main Aug 18, 2026
59 checks passed
@mdrxy
Mason Daugherty (mdrxy) deleted the johannes117/code/smooth-transcript-history branch August 18, 2026 02:41
Mason Daugherty (mdrxy) added a commit that referenced this pull request Aug 18, 2026
Conflict in `test_build_snapshot_message_count_reports_rendered_window`:
main's #5479 replaced the mounted-window expectation with
`min(INITIAL_WINDOW_SIZE, WINDOW_SIZE)` while this branch changed the
same assertion's string to carry turn counts. Both sides were needed,
so the resolution keeps main's `rendered` computation inside this
branch's `"N messages (M rendered), K turns"` format.

Main also added `MessageType.TOOL_GROUP`, which the new parametrized
`turn_count` test flagged on contact -- the enum-drift guard behaving
as intended. `TOOL_GROUP` is agent-authored, so it stays outside the
counted set; the case only needed `tool_group_messages` to construct.

`test_footers_render_for_hydrated_messages_above` fails on this merge,
but it fails identically on clean origin/main (verified by swapping in
main's copies of both source files), so it is pre-existing and
unrelated to these counts.
Mason Daugherty (mdrxy) pushed a commit that referenced this pull request Aug 18, 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.57](deepagents-code==0.1.56...deepagents-code==0.1.57)
(2026-08-18)

### Features

- Added warnings before expensive cold-cache turns and trust
user-declared endpoints for cold-cache policies
([#5439](#5439),
[#5462](#5462)).
- Made the chat input resizable by dragging its top border
([#5524](#5524)).
- Added a `multi_select` question type to `ask_user`
([#5097](#5097)).
- Added support for ACP approval modes
([#5394](#5394)).
- Added `DeepSeek-V4-Pro-0813` to the model picker
([#5512](#5512)).
- Show conversation turns alongside message counts
([#5571](#5571)).
- Include `TERM_PROGRAM` in the resume hint
([#5548](#5548)).

### Bug Fixes

- Report total context after `/offload`
([#5488](#5488)).
- Fixed transcript and thread restoration issues, including hydration
lag, scrolling resumed threads to the bottom, and hiding empty
previous-thread hints
([#5479](#5479),
[#5543](#5543),
[#5552](#5552)).
- Fixed Auto-mode approval handling by binding “yes” to the paired
`ask_user` question and avoiding duplicate Auto denial notices
([#5038](#5038),
[#5501](#5501)).
- Improved reload behavior by keeping the chat input responsive during
`/reload`, reporting MCP server changes, and avoiding plugin reload
prompt flashes or startup hints
([#5529](#5529),
[#5504](#5504),
[#5500](#5500),
[#5502](#5502)).
- Improved dependency update UI by preserving editable fields and hiding
dependency details after updates
([#5521](#5521),
[#5519](#5519)).
- Fixed chat UI polish issues, including detached spacer mount anchors,
the unfocused input cursor, and relative timestamp toggle display
([#5516](#5516),
[#5258](#5258),
[#5503](#5503)).
- Refresh the splash version after updates
([#5520](#5520)).

_End release notes preview._

---

> [!NOTE]
> A **community contributors** list and a **Special thanks** section
(crediting the users who filed the issues this release's PRs closed) are
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 3).

---------

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>
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: XL 1000+ LOC

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants