The broad candidate leg was replaced with an empty array in
`matchMode: "exact"`, so candidates came only from the server-side
`subject ~ query` filter and any work package whose match lived in a
description or a comment was unreachable. That inverts the parameter's
own guidance: exact mode is advertised for literal strings known
verbatim — error codes, ticket refs, commit SHAs — which are exactly the
strings that live in descriptions and comments rather than subjects.
Spec section 3.3 mandates two parallel requests with no exact-mode
carve-out.
Both legs now also page the candidate window in batches of 100 up to the
unchanged 250 cap, the way meeting search already does. Every tool shape
in this repo caps `pageSize` at 100 and OpenProject enforces a
server-side maximum, so a single `pageSize: 250` request could silently
return fewer candidates than the code claimed, with `response.total`
ignored so nothing detected it.
Also corrects `offset` on the search page from an item index to the
1-based page number its schema documents, and drops the duplicate
`items` array from `WorkPackageSearchPage` (kept on `PaginatedResult`
and the non-search list paths).
The existing `workPackageFixtureClient` returns every element for any
collection request and so cannot see a subject filter at all, which is
why this was invisible to the suite. New tests use a fixture that
honours the filter server-side; the old fixture is untouched.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The union candidate fetch wrapped both listWorkPackages() legs in an
unconditional .catch(() => ({ items: [] })), swallowing 401/403/429 and
OpenProjectAuthenticationError along with genuinely transient failures. An
expired token would silently degrade to "0 candidates", and the LLM would
report work packages as not found rather than surfacing the auth failure.
Replace the inline catches with a shared listCandidates() helper that uses
isFatalSearchError (already used by searchPipeline) to rethrow fatal errors
and only swallow non-fatal ones, preserving the property that a bad response
on one leg doesn't kill the other leg's candidates.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Registers openproject_search_work_packages (readOnly: true), taking the tool
registry from 18 to 19. searchWorkPackages now unions a precise server-side
subject filter with a broad recency-ordered candidate window, ranks via the
shared search pipeline, and deep-enriches top candidates with description and
comment text. Exact-mode requests skip the broad query entirely.
searchWorkPackages's return type changes from a flat PaginatedResult<WorkPackageSummary>
to WorkPackageSearchPage, nesting the summary under `workPackage` alongside
score, matchedFields, and an optional snippet.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>