Skip to content

perf(mcp-gateway): t3_list_threads returns the latest 20 chats unless asked for more - #176

Merged
jayleaton merged 1 commit into
mainfrom
perf/list-threads-bounded
Oct 31, 2026
Merged

jayleaton merged 1 commit into
mainfrom
perf/list-threads-bounded

Conversation

@jayleaton

Copy link
Copy Markdown
Owner

Summary

The gateway MCP tool t3_list_threads returned every chat on every environment, with full profile snapshots and model selections, and had no limit or cursor. One unfiltered call returned 7.4 MB. It now returns the 20 most recently updated chats by default.

 t3_list_threads(input)
-  per env: listThreads → filter → tag (full rows) ; includeQuestions → getThread for every waiting chat
-  merge all envs → return everything
+  per env: listThreads → filter (incl. time window) → compact row → drop rows at/before cursor → sort → keep limit+1
+  merge → sort (updatedAt desc, environmentId, id) → take limit → includeQuestions reads only these rows
+  → { items, hasMore, nextCursor, snapshotAt, environments? }
  • New inputs: limit (1–100, default 20), cursor (a keyset cursor over updatedAt, environmentId, and id, so pages never overlap), and updatedAfter, updatedBefore, and createdAfter (ISO). The existing filters are unchanged.
  • Rows are compact. profileSnapshot becomes profileId and profileName, and modelSelection becomes providerInstanceId and model. latestRunId and activeRunId are dropped. t3_get_thread returns the full chat.
  • Filters are not pushed into the port. listThreads reads the client's in-memory shell snapshot and accepts no filter arguments. Each environment filters and trims to one page before the merge, so the output stays bounded.
  • Fixes the stale t3_task_watch sentence "Wakes already arrive as messages". Task changes never wake the owner; the call only holds the current turn.
  • Updates docs/user/remote-access.md (defaults, paging, and the row fields).

The server's t3_thread_list is untouched.

Evidence

  • Before (fork/main tools.ts) and After (this branch) on the same synthetic fixture, 2 environments × 1,000 chats, t3_list_threads {}, run through callGatewayTool with node:
    before rows: 2000 bytes: 1925585
    after rows: 20 bytes: 10870
    
  • vp test run src/tools.test.ts src/server.test.ts in packages/mcp-gateway: 125 passed. The new bounded thread list tests cover: the default of 20 newest first; limit with cursor paging through 60 chats whose timestamps tie across environments, with no overlap or gaps; the time windows; combined filters across two environments before truncation; invalid limit, cursor, and time inputs; and includeQuestions reading only rows on the page.
  • tsc --noEmit (mcp-gateway), vp lint, and vp fmt --check on the touched files: clean.
  • Not run: the mcp-tool-use eval. Its cases exercise the server's orchestrator tools, not the gateway catalog.

Merge Danger

Door: two-way

Stateless read tool; a revert restores the old output.

Blast Radius: consumers

Agents that call the gateway's t3_list_threads without paging now see at most 20 chats, and rows no longer carry profileSnapshot, modelSelection, latestRunId, or activeRunId. Nothing in the repo reads those fields from this tool's output.

🤖 Generated with Claude Code

… asked for more

t3_list_threads fetched every chat on every environment and returned all of
them, with full profile snapshots and model selections; one unfiltered call
returned 7.4 MB.

It now sorts newest update first and returns 20 compact rows by default, with
limit (max 100) and a keyset cursor (nextCursor/hasMore) to page further.
updatedAfter, updatedBefore and createdAfter pick a time window. Each
environment filters and trims to one page before the merge, and
includeQuestions reads details only for the rows returned.

Also corrects the t3_task_watch description: task changes never wake the
owner; the call only holds the current turn.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L labels Oct 31, 2026
@github-actions

Copy link
Copy Markdown

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

Provider Metric Main baseline This PR Impact PR ceiling
Codex Total thread wire 5.0 KiB 5.0 KiB 0 B (0.0%) 6.8 KiB ✅
Codex Thread snapshot wire 3.8 KiB 3.8 KiB 0 B (0.0%) 4.9 KiB ✅
Codex Live turn WebSocket wire 1.2 KiB 1.2 KiB 0 B (0.0%) 2.0 KiB ✅
Codex Live turn WebSocket decoded 21.6 KiB 21.6 KiB 0 B (0.0%) 24.4 KiB ✅
Codex Live turn messages 2 2 0 (0.0%) 8 ✅
Claude Total thread wire 5.0 KiB 5.0 KiB −29 B (−0.6%) 6.8 KiB ✅
Claude Thread snapshot wire 3.8 KiB 3.8 KiB 0 B (0.0%) 4.9 KiB ✅
Claude Live turn WebSocket wire 1.3 KiB 1.2 KiB −29 B (−2.3%) 2.0 KiB ✅
Claude Live turn WebSocket decoded 21.9 KiB 21.9 KiB −41 B (−0.2%) 24.4 KiB ✅
Claude Live turn messages 2 1 −1 (−50.0%) 8 ✅

Baseline: a2160b9 · PR result: 1ce0f1c · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 108.5 KiB
  • Claude decoded thread snapshot: 108.8 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

@jayleaton
jayleaton marked this pull request as ready for review October 31, 2026 02:48
@jayleaton
jayleaton merged commit e2d81ca into main Oct 31, 2026
26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant