NVIDIA / NVIDIA/TensorRT-LLM

[Bug]: Responses history trimming hangs on over-capacity input without a completed turn

Open
#18,759 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Inference runtime
Dominant language
Python
Stars
14.7k
Forks
2.8k
Avg merge
2d 23h
Merged PRs (30d)
489

Description

With the default history capacity, storing 65 consecutive user messages can enter a non-yielding loop in ConversationHistoryStore. Harmony histories without a final message have the same failure mode.

System Info
  • TensorRT-LLM: main, commit 9964d34d67add61764ff1047bc76992fde376518 (also the current main revision checked before filing).
  • Local component validation: macOS arm64, Python 3.12.14, pytest 9.1.1, pytest-asyncio 1.4.0, openai-harmony 0.0.8.
  • No TensorRT/CUDA container, model, or GPU was used. This is a source-level component reproduction, not an end-to-end serving result. Native TensorRT-LLM pytest remains unverified.
Information
  • The official example scripts
  • My own modified scripts: an isolated harness executing the unmodified upstream history-store class.
Tasks
  • An officially supported task in the examples folder
  • My own task or dataset: Responses API conversation-history capacity handling with synthetic user messages.
Reproduction

The corresponding minimal call in an environment where TensorRT-LLM is importable is:

import asyncio

from tensorrt_llm.serve.responses_utils import ConversationHistoryStore


async def main():
    store = ConversationHistoryStore()
    messages = [
        {"role": "user", "content": f"input {i}"}
        for i in range(store.conversation_capacity + 1)
    ]
    print("enter store_messages: 65 messages; capacity=64", flush=True)
    await store.store_messages("resp_probe", messages, prev_resp_id=None)
    print("returned", flush=True)


asyncio.run(main())

Run this in a separate process with an external timeout. asyncio.wait_for() cannot interrupt the synchronous loop because it never yields.

For the Harmony variant, construct the messages with Message.from_role_and_content("user", f"input {i}") from openai_harmony instead of dictionaries.

Locally, I loaded the exact upstream ConversationHistoryStore definition and its UUID/logging helpers via AST, postponing annotation evaluation and bypassing TensorRT-LLM package initialization. The Harmony checks used real openai_harmony.Message objects. The direct-import snippet above was not executed in that form locally.

Expected behavior

Trimming should terminate when a conversation exceeds capacity, including when no completed turn is available. The incomplete-turn case should have an explicit eviction or rejection policy.

Actual behavior

In the current trimming implementation, scanning 65 user messages leaves start_index == 64 and end_index == 0. Consequently, del conversation[64:1] removes nothing, and _trim_conversation() repeats indefinitely while holding conversations_lock.

Observed with the unmodified class:

  • Three consecutive calls to the deletion helper leave the message counts at 65 -> 65 -> 65 -> 65.
  • Separate processes executing store_messages() reach the call and exceed a two-second external timeout for both dictionary and Harmony user messages.
  • An alternating user/assistant control returns and trims 80 messages to 64.

The ordinary input-conversion path also preserves these consecutive user messages before storage. No HTTP-server hang or GPU result is claimed here.

Additional notes

I have prepared a local fix that distinguishes a missing end index from index zero. When no completed turn exists, it removes the oldest non-system/developer message, retaining recent input and instructions where capacity permits. If only instructions remain above capacity, it removes the oldest instruction. Completed-turn trimming keeps its existing behavior.

The focused test file has 19 cases: the unmodified class produces 7 failed / 12 passed, and the local fix produces 19 passed in the isolated CPU runner. Coverage includes ordinary and Harmony messages, instruction prefixes, capacity boundaries, completed turns, instruction-only histories, and Harmony output without a final channel. Native TensorRT-LLM pytest and upstream CI are still pending.

Related work: merged PR #15043 fixed capacity enforcement after writes and trimming through unmapped response IDs. Its complete-turn tests do not cover this empty-deletion case. I also checked open PR #18497; its inspected patch changes Responses serving paths but does not modify ConversationHistoryStore.

Would the proposed incomplete-turn retention policy be acceptable for a focused PR? I am filing this issue first in accordance with the contribution process.

Before submitting a new issue...
  • Searched existing and past issues/PRs and reviewed the Responses API documentation and examples for relevant guidance.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in tensorrt_llm/serve/responses_utils.py, especially _trim_conversation() and ConversationHistoryStore, and reproduce the 65-message case with the isolated CPU harness. Check the focused 19-case suite for ordinary and Harmony messages, instruction prefixes, capacity boundaries, and incomplete histories. Done means trimming terminates at capacity without a completed turn while preserving the existing completed-turn behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
api, backend
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
78/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.