What happened
Desktop history search loads and decodes a complete conversation before checking its content for matches. The result limit stops matching only after that work has finished.
On a deterministic 20,000-message transcript, requesting one result when the oldest message already matches still decodes all 20,000 messages and requests 78 more pages after the opening bootstrap. The same unnecessary loading occurs with the UI's limit=10 when the first ten messages match.
Expected: read messages in the existing oldest-first order, and stop requesting further pages once the result or snippet budget is satisfied. A result at the end of a page should preserve the truncation flag without fetching another page. Small conversations already present in the bootstrap should need no extra request.
How to reproduce
- Use a synthetic transcript with 20,000 small user messages, each in a separate Turn. Put a unique query term in the oldest message only; keep the session title unrelated to the query.
- Exercise the real Desktop
search:thread handler with limit=1, using the shared ClientSessionSubscription and Host transcript pager. Use the existing 16 KiB tail bootstrap and 512 KiB / 256-message page limits.
- Count transcript page requests and completed message decodes before the matcher sees the first candidate.
- Repeat with the first ten messages matching and
limit=10 to cover the actual UI result limit.
Observed on the source baseline below:
| Scenario |
Results requested |
Complete message decodes |
Requests after bootstrap |
| Oldest message matches |
1 |
20,000 |
78 |
| First ten messages match |
10 |
20,000 |
78 |
For the first scenario, all 20,000 decodes and 78 requests have already happened when the first candidate is checked. The fixture transfers 4,106,558 raw message bytes, excluding base64, protocol framing and the session snapshot. Page counts depend on message sizes and Turn boundaries; 78 is this fixture's result, not a fixed protocol property.
The linked fix includes a regression at the real handler → matcher → subscription → pager/decoder boundary in apps/desktop/src/main/__tests__/thread-search-pagination.test.ts. Its synthetic transcript reproduces the excessive decoding and checks that the result limit stops pagination. No private conversations are needed.
Environment
- Source baseline:
d3292393c575d2019ca406a5048099abf3399e0f.
- macOS 15.7.3, Apple Silicon.
- Surface: Desktop history search through Runtime Host.
- Node.js v24.14.0.
- Measurements use synthetic transcript storage/transport and the real search/paging/decoding code; they do not measure production network, disk or UI latency.
Logs, screenshots, or additional context
The Core dependency returns Promise<StoredMessage[] | null>. The Desktop adapter awaits session.loadTranscript() before runThreadSearch can match content. The existing paging protocol already supports forward reads from the oldest position at a fixed watermark.
Related to #2913 and #4677. This is a focused part of their search-performance work; it does not require implementing Host-local indexed search. #5255 / #5256 fixed abandoned-query cancellation, while this defect occurs in a valid query whose earliest messages already satisfy its result limit.
Preserve title priority, session ordering, redaction/privacy checks, source message identities, cancellation and ordinary read-failure behavior. No-match or insufficient-match searches still need a full scan without an index.
Prepared and submitted by Codex on behalf of @liuxiaocs7. Codex contributed diagnosis, the synthetic reproduction, implementation and this report.
What happened
Desktop history search loads and decodes a complete conversation before checking its content for matches. The result limit stops matching only after that work has finished.
On a deterministic 20,000-message transcript, requesting one result when the oldest message already matches still decodes all 20,000 messages and requests 78 more pages after the opening bootstrap. The same unnecessary loading occurs with the UI's
limit=10when the first ten messages match.Expected: read messages in the existing oldest-first order, and stop requesting further pages once the result or snippet budget is satisfied. A result at the end of a page should preserve the truncation flag without fetching another page. Small conversations already present in the bootstrap should need no extra request.
How to reproduce
search:threadhandler withlimit=1, using the sharedClientSessionSubscriptionand Host transcript pager. Use the existing 16 KiB tail bootstrap and 512 KiB / 256-message page limits.limit=10to cover the actual UI result limit.Observed on the source baseline below:
For the first scenario, all 20,000 decodes and 78 requests have already happened when the first candidate is checked. The fixture transfers 4,106,558 raw message bytes, excluding base64, protocol framing and the session snapshot. Page counts depend on message sizes and Turn boundaries; 78 is this fixture's result, not a fixed protocol property.
The linked fix includes a regression at the real handler → matcher → subscription → pager/decoder boundary in
apps/desktop/src/main/__tests__/thread-search-pagination.test.ts. Its synthetic transcript reproduces the excessive decoding and checks that the result limit stops pagination. No private conversations are needed.Environment
d3292393c575d2019ca406a5048099abf3399e0f.Logs, screenshots, or additional context
The Core dependency returns
Promise<StoredMessage[] | null>. The Desktop adapter awaitssession.loadTranscript()beforerunThreadSearchcan match content. The existing paging protocol already supports forward reads from the oldest position at a fixed watermark.Related to #2913 and #4677. This is a focused part of their search-performance work; it does not require implementing Host-local indexed search. #5255 / #5256 fixed abandoned-query cancellation, while this defect occurs in a valid query whose earliest messages already satisfy its result limit.
Preserve title priority, session ordering, redaction/privacy checks, source message identities, cancellation and ordinary read-failure behavior. No-match or insufficient-match searches still need a full scan without an index.
Prepared and submitted by Codex on behalf of @liuxiaocs7. Codex contributed diagnosis, the synthetic reproduction, implementation and this report.