[GH-ISSUE #177] Critical Bug: Unbounded tool responses can exceed LLM request limits and permanently poison chats #104

Open
opened 2026-10-18 21:10:48 +00:00 by bsakaguchi · 1 comment
Owner

Originally created by @Classic298 on GitHub (Sep 17, 2026).
Original GitHub issue: https://github.com/open-webui/open-terminal/issues/177

Version

Description

Several model-facing Open Terminal endpoints can return arbitrarily large, or very large, response bodies. Result-count limits such as limit, max_results, and tail do not reliably limit serialized response bytes because one result, one line, or one process-output entry can itself be very large.

When Open Terminal is connected to an LLM client, the client normally serializes the response as a tool result and appends it to the next model request. One oversized response can therefore:

  1. exceed the model provider's HTTP/message-size limit;
  2. make the immediate tool continuation fail;
  3. be persisted in chat history;
  4. make all subsequent continuations and retries fail because the same oversized tool result is sent again;
  5. produce large database rows, browser events, proxy traffic, memory usage, and misleading secondary timeout/authentication errors.

This happened in production with match_files. It is not only a theoretical resource-usage concern: one normal search call made the affected conversation unusable until its database row was repaired.

Production incident

match_files searched minified, single-line JSON files. Only nine results were returned, but two matching lines were approximately 15.7 MB and 17.6 MB.

Observed sizes:

  • raw match_files JSON response/tool result: 36,734,472 bytes;
  • persisted function_call_output after nested JSON escaping: approximately 43.7 MB;
  • subsequent model-provider request: 44,133,583 bytes;
  • provider request limit: 30 MB.

The provider rejected the continuation with:

The message size (44133583 bytes) exceeds 30.000MB limit.

Because the full result had already become part of chat history, reloading or retrying did not recover the conversation.

Root cause in match_files

The returned match contains the entire physical line

content_match() returns text without any length bound:

def content_match(text: str, line: int) -> dict | None:
    text = text.rstrip("\r\n")
    index = text.lower().find(query_lower)
    ...
    return {"line": line, "column": column, "text": text}

A minified JSON document is commonly one physical line, so a single match can return the complete file.

The ripgrep path bypasses the Python fallback's 1 MiB file-size check

The ripgrep path invokes rg --json without a maximum file-size or maximum returned-line-size control (main.py lines 1183-1226). It then forwards data["lines"]["text"] to content_match().

The MAX_CONTENT_SEARCH_FILE_SIZE = 1 MiB check exists only in the Python fallback (main.py lines 1228-1250). Therefore behavior depends on whether rg is installed:

  • without rg: files larger than 1 MiB are skipped;
  • with rg: files larger than 1 MiB are searched, and complete matching lines are returned.

Pagination limits result count, not response bytes

limit is capped at 100, and matches per file are capped at 3, but each match's text is unbounded. The final slice therefore does not bound the serialized payload (main.py lines 1279-1304).

Even the Python fallback can theoretically return close to 100 MiB of line text: the page can contain up to 100 matching files, each just under the 1 MiB search limit, before JSON escaping overhead.

Minimal reproduction

Run Open Terminal in an environment where rg is installed, then create a file containing one 2 MiB line:

mkdir -p /tmp/open-terminal-output-repro
python - <<'PY'
from pathlib import Path
Path('/tmp/open-terminal-output-repro/large.json').write_text(
    'needle' + ('x' * (2_000_000 - len('needle'))),
    encoding='utf-8',
)
PY

Call match_files:

curl -sS -G \
  -H "Authorization: Bearer $OPEN_TERMINAL_API_KEY" \
  --data-urlencode 'query=needle' \
  --data-urlencode 'path=/tmp/open-terminal-output-repro' \
  http://localhost:8000/files/matches \
  -o /tmp/match-result.json

wc -c /tmp/match-result.json

The response is approximately 2 MB even though it contains one result. Increasing the one-line file to 35 MB creates a response of roughly the same size as the incident.

Audit of the other model-facing tools

I audited all operations currently included in the OpenAPI schema at the commit above. The same class of bug exists in several other endpoints.

Operation Risk Finding
read_file Critical Reads and returns the entire text file when no line range is supplied. A requested line range still reads the entire file into memory first. A single requested line can be arbitrarily large. Document extractors also return complete extracted text. Allowed binary MIME types are returned in full and clients may base64-encode them, adding about 33% overhead.
grep_search Critical max_results limits matches to 500 but every match contains the complete matching line with no byte/column cap. One minified JSON line can create a multi-megabyte response; 500 large lines can be much worse. It also searches files without a file-size cap.
match_files Critical / confirmed incident Complete matching lines are returned. The rg implementation bypasses the fallback's 1 MiB file cap. Count pagination does not cap bytes.
run_command Critical If wait is used and the command completes, all retained process-output entries are returned unless the caller explicitly supplies tail. The process log defaults to a 50 MB cap, which is larger than common model-provider request limits and is not an API response-size cap.
get_process_status Critical Defaults to offset=0 and tail=None, so it can return the entire retained process log. tail limits entry count rather than serialized response bytes; the normal runners read bounded chunks, but there is still no explicit API byte invariant.
list_files High Returns every entry in a directory with no pagination, entry-count limit, or serialized-byte limit.
list_processes Medium Returns every tracked process for a session with no pagination. Process creation is not capped here, and the complete command string is reflected in every entry.
get_info Low / operator-controlled Returns OPEN_TERMINAL_INFO verbatim with no limit. This is configuration-controlled rather than filesystem/command-controlled, but can still become an oversized tool result.
search_files Low Result count is capped at 100 and results contain metadata rather than file contents. A common global byte guard would still be useful.
glob_search Low–Medium Result count is capped at 500 and results contain paths/metadata. Long paths multiplied by 500 can still produce a material response, but this is substantially better bounded than content-returning tools.
read_user_terminal Low Returns a rolling terminal buffer. The buffer is explicitly capped to the latest 65,536 bytes, so this endpoint already demonstrates the desired bounded-output behavior.
display_file Low Returns only resolved path and existence metadata.
write_file Low Returns only path and byte count. The request body may be large, but the response is bounded metadata.
replace_file_content Low Returns only path and byte count. The implementation reads the complete file in memory, but does not return it as tool output.
send_process_input Low Returns {"status":"ok"}.
kill_process Low Returns bounded status metadata.
send_user_terminal_input Low Returns bounded status metadata.

Supporting source references

read_file

Each result stores line.rstrip("\n\r") in full (main.py lines 1024-1047). max_results is capped by count only (main.py lines 995-997).

A local source-level reproduction confirmed that a single 2,000,000-character line produces a serialized grep_search result of approximately 2,000,229 bytes.

run_command and get_process_status

A local test against read_log() confirmed that tail=None with twenty 1,000,000-character records serializes approximately 20,000,640 bytes. A synthetic large single record also confirms that tail is an entry-count control rather than a byte-budget control. The normal process runners usually write smaller chunks (for example, 4 KiB PTY reads), so the primary practical risk is the default unbounded number of retained entries.

list_files

fs.listdir(target) collects every os.listdir() entry and the endpoint returns the complete list (main.py lines 652-663, utils/fs.py lines 202-223).

The primary issue is the model-facing OpenAPI tool surface above. There are also other response/resource paths worth applying the same safety principles to:

  • read_skill returns the complete SKILL.md body with no content-size cap, although resource names are bounded to 50 entries.
  • execute_notebook_cell serializes complete Jupyter cell outputs, including stdout, rich display data, images, and tracebacks, without a response-byte cap. These notebook routes currently use include_in_schema=False, but the risk applies to clients that call them directly or adapters that expose them.
  • /files/compare can return a large, structurally amplified diff. Inputs allow up to two million extracted characters / 50,000 lines per file, and line content can appear in both content and segments.
  • /proxy/{port}/{path} buffers and returns the complete upstream body.
  • /files/view and /files/archive return complete files/archives and intentionally serve UI/download use cases; they should preferably stream rather than buffer entire payloads in memory.
  • /files/upload reads the complete upload into memory before writing it.

These endpoints do not all need LLM-style truncation, but they should have explicit streaming, request, response, or memory limits appropriate to their intended use.

Expected behavior

No model-facing Open Terminal tool should be able to return a response large enough to exceed typical LLM provider limits or poison a client's persistent conversation merely because a file has a long line, a directory has many entries, or a command prints a lot of output.

Count limits should not be presented as sufficient response limits when an individual item has unbounded size.

Suggested fix

1. Add a shared model-tool output byte budget

Add a configurable server-side limit, measured after UTF-8 JSON serialization, for example:

OPEN_TERMINAL_MAX_TOOL_OUTPUT_BYTES=262144

Every endpoint included as an LLM/OpenAPI/MCP tool should either:

  • produce a response under this limit;
  • return a structured, bounded truncation result; or
  • save the complete result to a file/artifact and return a bounded reference.

Example overflow metadata:

{
  "truncated": true,
  "reason": "tool_output_byte_limit",
  "original_bytes": 36734472,
  "returned_bytes": 262144,
  "next_offset": 12345,
  "message": "Output exceeded the configured tool response limit. Request a narrower range or read the saved result in chunks."
}

The check should account for UTF-8 and JSON escaping. Truncating a Python string to N characters is not enough.

2. Fix each high-risk endpoint semantically

  • match_files / grep_search: return a bounded excerpt around each match instead of the full physical line; include original line length and an excerpt_truncated flag. Enforce the same file-size behavior with and without rg. Stop accumulating results once the global byte budget is exhausted.
  • read_file: add max_bytes/chunked reads and continuation metadata. Default to a safe maximum even when no line range is supplied. Do not read the complete file merely to serve a small range. For binary content, return a file reference/metadata to model clients rather than an unbounded raw response or data URL.
  • run_command / get_process_status: impose a maximum response byte count independent of log-retention size. Return the newest or next chunk with a cursor. Keep tail, but make the final selection byte-bounded too.
  • list_files: add pagination/cursor and maximum entries, then apply the global byte cap.
  • list_processes: cap/paginate results and consider a per-session tracked-process limit.
  • get_info: validate/cap configured content or exclude it from the shared limit only with an explicit administrator opt-in.

3. Add observability

Log and/or emit a metric whenever output is truncated, including:

  • operation/tool name;
  • original and returned byte counts;
  • number of returned/omitted entries;
  • process ID or path where appropriate;
  • whether a continuation cursor or artifact was created.

Do not log the oversized content itself.

Originally created by @Classic298 on GitHub (Sep 17, 2026). Original GitHub issue: https://github.com/open-webui/open-terminal/issues/177 ## Version - Open Terminal: `v0.13.0` - Audited commit: [`2085cd819edcac52873dcfe1fc3e722cadc14162`](https://github.com/open-webui/open-terminal/commit/2085cd819edcac52873dcfe1fc3e722cadc14162) - Integration where this occurred: Open WebUI using Open Terminal's OpenAPI tools through a Terminals orchestrator ## Description Several model-facing Open Terminal endpoints can return arbitrarily large, or very large, response bodies. Result-count limits such as `limit`, `max_results`, and `tail` do not reliably limit serialized response **bytes** because one result, one line, or one process-output entry can itself be very large. When Open Terminal is connected to an LLM client, the client normally serializes the response as a tool result and appends it to the next model request. One oversized response can therefore: 1. exceed the model provider's HTTP/message-size limit; 2. make the immediate tool continuation fail; 3. be persisted in chat history; 4. make all subsequent continuations and retries fail because the same oversized tool result is sent again; 5. produce large database rows, browser events, proxy traffic, memory usage, and misleading secondary timeout/authentication errors. This happened in production with `match_files`. It is not only a theoretical resource-usage concern: one normal search call made the affected conversation unusable until its database row was repaired. ## Production incident `match_files` searched minified, single-line JSON files. Only nine results were returned, but two matching lines were approximately 15.7 MB and 17.6 MB. Observed sizes: - raw `match_files` JSON response/tool result: **36,734,472 bytes**; - persisted `function_call_output` after nested JSON escaping: approximately **43.7 MB**; - subsequent model-provider request: **44,133,583 bytes**; - provider request limit: **30 MB**. The provider rejected the continuation with: ```text The message size (44133583 bytes) exceeds 30.000MB limit. ``` Because the full result had already become part of chat history, reloading or retrying did not recover the conversation. ## Root cause in `match_files` ### The returned match contains the entire physical line [`content_match()` returns `text` without any length bound](https://github.com/open-webui/open-terminal/blob/2085cd819edcac52873dcfe1fc3e722cadc14162/open_terminal/main.py#L1123-L1129): ```python def content_match(text: str, line: int) -> dict | None: text = text.rstrip("\r\n") index = text.lower().find(query_lower) ... return {"line": line, "column": column, "text": text} ``` A minified JSON document is commonly one physical line, so a single match can return the complete file. ### The ripgrep path bypasses the Python fallback's 1 MiB file-size check The ripgrep path invokes `rg --json` without a maximum file-size or maximum returned-line-size control ([`main.py` lines 1183-1226](https://github.com/open-webui/open-terminal/blob/2085cd819edcac52873dcfe1fc3e722cadc14162/open_terminal/main.py#L1183-L1226)). It then forwards `data["lines"]["text"]` to `content_match()`. The `MAX_CONTENT_SEARCH_FILE_SIZE = 1 MiB` check exists only in the Python fallback ([`main.py` lines 1228-1250](https://github.com/open-webui/open-terminal/blob/2085cd819edcac52873dcfe1fc3e722cadc14162/open_terminal/main.py#L1228-L1250)). Therefore behavior depends on whether `rg` is installed: - without `rg`: files larger than 1 MiB are skipped; - with `rg`: files larger than 1 MiB are searched, and complete matching lines are returned. ### Pagination limits result count, not response bytes `limit` is capped at 100, and matches per file are capped at 3, but each match's `text` is unbounded. The final slice therefore does not bound the serialized payload ([`main.py` lines 1279-1304](https://github.com/open-webui/open-terminal/blob/2085cd819edcac52873dcfe1fc3e722cadc14162/open_terminal/main.py#L1279-L1304)). Even the Python fallback can theoretically return close to 100 MiB of line text: the page can contain up to 100 matching files, each just under the 1 MiB search limit, before JSON escaping overhead. ## Minimal reproduction Run Open Terminal in an environment where `rg` is installed, then create a file containing one 2 MiB line: ```bash mkdir -p /tmp/open-terminal-output-repro python - <<'PY' from pathlib import Path Path('/tmp/open-terminal-output-repro/large.json').write_text( 'needle' + ('x' * (2_000_000 - len('needle'))), encoding='utf-8', ) PY ``` Call `match_files`: ```bash curl -sS -G \ -H "Authorization: Bearer $OPEN_TERMINAL_API_KEY" \ --data-urlencode 'query=needle' \ --data-urlencode 'path=/tmp/open-terminal-output-repro' \ http://localhost:8000/files/matches \ -o /tmp/match-result.json wc -c /tmp/match-result.json ``` The response is approximately 2 MB even though it contains one result. Increasing the one-line file to 35 MB creates a response of roughly the same size as the incident. ## Audit of the other model-facing tools I audited all operations currently included in the OpenAPI schema at the commit above. The same class of bug exists in several other endpoints. | Operation | Risk | Finding | |---|---:|---| | `read_file` | **Critical** | Reads and returns the entire text file when no line range is supplied. A requested line range still reads the entire file into memory first. A single requested line can be arbitrarily large. Document extractors also return complete extracted text. Allowed binary MIME types are returned in full and clients may base64-encode them, adding about 33% overhead. | | `grep_search` | **Critical** | `max_results` limits matches to 500 but every match contains the complete matching line with no byte/column cap. One minified JSON line can create a multi-megabyte response; 500 large lines can be much worse. It also searches files without a file-size cap. | | `match_files` | **Critical / confirmed incident** | Complete matching lines are returned. The `rg` implementation bypasses the fallback's 1 MiB file cap. Count pagination does not cap bytes. | | `run_command` | **Critical** | If `wait` is used and the command completes, all retained process-output entries are returned unless the caller explicitly supplies `tail`. The process log defaults to a 50 MB cap, which is larger than common model-provider request limits and is not an API response-size cap. | | `get_process_status` | **Critical** | Defaults to `offset=0` and `tail=None`, so it can return the entire retained process log. `tail` limits entry count rather than serialized response bytes; the normal runners read bounded chunks, but there is still no explicit API byte invariant. | | `list_files` | **High** | Returns every entry in a directory with no pagination, entry-count limit, or serialized-byte limit. | | `list_processes` | **Medium** | Returns every tracked process for a session with no pagination. Process creation is not capped here, and the complete command string is reflected in every entry. | | `get_info` | **Low / operator-controlled** | Returns `OPEN_TERMINAL_INFO` verbatim with no limit. This is configuration-controlled rather than filesystem/command-controlled, but can still become an oversized tool result. | | `search_files` | Low | Result count is capped at 100 and results contain metadata rather than file contents. A common global byte guard would still be useful. | | `glob_search` | Low–Medium | Result count is capped at 500 and results contain paths/metadata. Long paths multiplied by 500 can still produce a material response, but this is substantially better bounded than content-returning tools. | | `read_user_terminal` | Low | Returns a rolling terminal buffer. The buffer is explicitly capped to the latest 65,536 bytes, so this endpoint already demonstrates the desired bounded-output behavior. | | `display_file` | Low | Returns only resolved path and existence metadata. | | `write_file` | Low | Returns only path and byte count. The request body may be large, but the response is bounded metadata. | | `replace_file_content` | Low | Returns only path and byte count. The implementation reads the complete file in memory, but does not return it as tool output. | | `send_process_input` | Low | Returns `{"status":"ok"}`. | | `kill_process` | Low | Returns bounded status metadata. | | `send_user_terminal_input` | Low | Returns bounded status metadata. | ### Supporting source references #### `read_file` - Complete text is loaded with `await fs.read_text(target)` and then split into lines: [`main.py` lines 702-705](https://github.com/open-webui/open-terminal/blob/2085cd819edcac52873dcfe1fc3e722cadc14162/open_terminal/main.py#L702-L705). - Complete extracted document text is returned for the requested line range, whose default is all lines: [`main.py` lines 712-727](https://github.com/open-webui/open-terminal/blob/2085cd819edcac52873dcfe1fc3e722cadc14162/open_terminal/main.py#L712-L727). - Allowed binary types are returned in full: [`main.py` lines 729-731](https://github.com/open-webui/open-terminal/blob/2085cd819edcac52873dcfe1fc3e722cadc14162/open_terminal/main.py#L729-L731). - Ordinary text defaults to the complete file: [`main.py` lines 739-745](https://github.com/open-webui/open-terminal/blob/2085cd819edcac52873dcfe1fc3e722cadc14162/open_terminal/main.py#L739-L745). #### `grep_search` Each result stores `line.rstrip("\n\r")` in full ([`main.py` lines 1024-1047](https://github.com/open-webui/open-terminal/blob/2085cd819edcac52873dcfe1fc3e722cadc14162/open_terminal/main.py#L1024-L1047)). `max_results` is capped by count only ([`main.py` lines 995-997](https://github.com/open-webui/open-terminal/blob/2085cd819edcac52873dcfe1fc3e722cadc14162/open_terminal/main.py#L995-L997)). A local source-level reproduction confirmed that a single 2,000,000-character line produces a serialized `grep_search` result of approximately 2,000,229 bytes. #### `run_command` and `get_process_status` - Both endpoints call `read_log()` with optional `tail`; the default is `None`: [`main.py` lines 1691-1749](https://github.com/open-webui/open-terminal/blob/2085cd819edcac52873dcfe1fc3e722cadc14162/open_terminal/main.py#L1691-L1749) and [`main.py` lines 1763-1808](https://github.com/open-webui/open-terminal/blob/2085cd819edcac52873dcfe1fc3e722cadc14162/open_terminal/main.py#L1763-L1808). - `read_log()` collects every matching output record when `tail=None`; when supplied, `tail` bounds only the `deque` entry count, not bytes: [`utils/log.py` lines 190-225](https://github.com/open-webui/open-terminal/blob/2085cd819edcac52873dcfe1fc3e722cadc14162/open_terminal/utils/log.py#L190-L225). - The per-process log default is 50,000,000 bytes: [`env.py` lines 92-99](https://github.com/open-webui/open-terminal/blob/2085cd819edcac52873dcfe1fc3e722cadc14162/open_terminal/env.py#L92-L99). A local test against `read_log()` confirmed that `tail=None` with twenty 1,000,000-character records serializes approximately 20,000,640 bytes. A synthetic large single record also confirms that `tail` is an entry-count control rather than a byte-budget control. The normal process runners usually write smaller chunks (for example, 4 KiB PTY reads), so the primary practical risk is the default unbounded number of retained entries. #### `list_files` `fs.listdir(target)` collects every `os.listdir()` entry and the endpoint returns the complete list ([`main.py` lines 652-663](https://github.com/open-webui/open-terminal/blob/2085cd819edcac52873dcfe1fc3e722cadc14162/open_terminal/main.py#L652-L663), [`utils/fs.py` lines 202-223](https://github.com/open-webui/open-terminal/blob/2085cd819edcac52873dcfe1fc3e722cadc14162/open_terminal/utils/fs.py#L202-L223)). ## Related non-OpenAPI/UI endpoints The primary issue is the model-facing OpenAPI tool surface above. There are also other response/resource paths worth applying the same safety principles to: - `read_skill` returns the complete `SKILL.md` body with no content-size cap, although resource names are bounded to 50 entries. - `execute_notebook_cell` serializes complete Jupyter cell outputs, including stdout, rich display data, images, and tracebacks, without a response-byte cap. These notebook routes currently use `include_in_schema=False`, but the risk applies to clients that call them directly or adapters that expose them. - `/files/compare` can return a large, structurally amplified diff. Inputs allow up to two million extracted characters / 50,000 lines per file, and line content can appear in both `content` and `segments`. - `/proxy/{port}/{path}` buffers and returns the complete upstream body. - `/files/view` and `/files/archive` return complete files/archives and intentionally serve UI/download use cases; they should preferably stream rather than buffer entire payloads in memory. - `/files/upload` reads the complete upload into memory before writing it. These endpoints do not all need LLM-style truncation, but they should have explicit streaming, request, response, or memory limits appropriate to their intended use. ## Expected behavior No model-facing Open Terminal tool should be able to return a response large enough to exceed typical LLM provider limits or poison a client's persistent conversation merely because a file has a long line, a directory has many entries, or a command prints a lot of output. Count limits should not be presented as sufficient response limits when an individual item has unbounded size. ## Suggested fix ### 1. Add a shared model-tool output byte budget Add a configurable server-side limit, measured after UTF-8 JSON serialization, for example: ```text OPEN_TERMINAL_MAX_TOOL_OUTPUT_BYTES=262144 ``` Every endpoint included as an LLM/OpenAPI/MCP tool should either: - produce a response under this limit; - return a structured, bounded truncation result; or - save the complete result to a file/artifact and return a bounded reference. Example overflow metadata: ```json { "truncated": true, "reason": "tool_output_byte_limit", "original_bytes": 36734472, "returned_bytes": 262144, "next_offset": 12345, "message": "Output exceeded the configured tool response limit. Request a narrower range or read the saved result in chunks." } ``` The check should account for UTF-8 and JSON escaping. Truncating a Python string to N characters is not enough. ### 2. Fix each high-risk endpoint semantically - **`match_files` / `grep_search`:** return a bounded excerpt around each match instead of the full physical line; include original line length and an `excerpt_truncated` flag. Enforce the same file-size behavior with and without `rg`. Stop accumulating results once the global byte budget is exhausted. - **`read_file`:** add `max_bytes`/chunked reads and continuation metadata. Default to a safe maximum even when no line range is supplied. Do not read the complete file merely to serve a small range. For binary content, return a file reference/metadata to model clients rather than an unbounded raw response or data URL. - **`run_command` / `get_process_status`:** impose a maximum response byte count independent of log-retention size. Return the newest or next chunk with a cursor. Keep `tail`, but make the final selection byte-bounded too. - **`list_files`:** add pagination/cursor and maximum entries, then apply the global byte cap. - **`list_processes`:** cap/paginate results and consider a per-session tracked-process limit. - **`get_info`:** validate/cap configured content or exclude it from the shared limit only with an explicit administrator opt-in. ### 3. Add observability Log and/or emit a metric whenever output is truncated, including: - operation/tool name; - original and returned byte counts; - number of returned/omitted entries; - process ID or path where appropriate; - whether a continuation cursor or artifact was created. Do not log the oversized content itself.
Author
Owner

@dirkenglund commented on GitHub (Sep 29, 2026):

Same class, second endpoint and a larger measurement, from a production Open WebUI + PostgreSQL deployment (Open Terminal 0.11.34; the code path is unchanged in 0.14.0):

  • grep_search: each match returns the whole line, "content": line.rstrip("\n\r") (open_terminal/main.py, _search_file). A call with max_results=30 over a workspace holding a few single-line files returned ~269 MB in one tool result.
  • The consequences went beyond one chat. Open WebUI stores the tool result in the chat row (58 MB compressed, 269 MB as JSON). Later:
    • loading that chat pushed the Open WebUI process past 3 GiB, and it was OOM-evicted (full outage for every user);
    • Open WebUI's chat search, which casts each chat's JSON to text, pushed PostgreSQL past its 4 GiB container limit, and it was OOM-killed.

Suggested fix, matching the byte-budget idea above: cap each match's content to a window around the match (for example 400 characters, with a truncated flag and the full line length), and cap the whole response by bytes, not only by result count. Clients can then ask for a specific file range if they need more.

Implementation by Claude (Anthropic) in collaboration with @dirkenglund.

<!-- gh-comment-id:5896383304 --> @dirkenglund commented on GitHub (Sep 29, 2026): Same class, second endpoint and a larger measurement, from a production Open WebUI + PostgreSQL deployment (Open Terminal 0.11.34; the code path is unchanged in 0.14.0): - **`grep_search`**: each match returns the whole line, `"content": line.rstrip("\n\r")` (`open_terminal/main.py`, `_search_file`). A call with `max_results=30` over a workspace holding a few single-line files returned **~269 MB** in one tool result. - The consequences went beyond one chat. Open WebUI stores the tool result in the chat row (58 MB compressed, 269 MB as JSON). Later: - loading that chat pushed the Open WebUI process past 3 GiB, and it was OOM-evicted (full outage for every user); - Open WebUI's chat search, which casts each chat's JSON to text, pushed PostgreSQL past its 4 GiB container limit, and it was OOM-killed. Suggested fix, matching the byte-budget idea above: cap each match's `content` to a window around the match (for example 400 characters, with a `truncated` flag and the full line length), and cap the whole response by bytes, not only by result count. Clients can then ask for a specific file range if they need more. Implementation by Claude (Anthropic) in collaboration with @dirkenglund.
Sign in to join this conversation.
No labels
bug
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
open-webui/open-terminal#104
No description provided.