Expand a bounded set of tasks by stable ID in one read (#172) - #203
Merged
Conversation
A progressive-expansion workflow -- read a compact page, then fetch notes for the few ambiguous captures -- previously issued one get_task call per item. One observed inbox-zero run made 28 scalar detail calls for a 32-item inbox. list_tasks.filter gains an optional ids array of 1-20 stable task IDs. Other filters still apply as an intersection, so an ID outside the supplied scope is reported in unresolvedIDs rather than returned, and an invalid ID never falls back to an unrelated page. Results follow the requested ID order. A bounded selection is one complete response: page.cursor is rejected, page.limit must cover the requested IDs, and no nextCursor is issued. IDs are trimmed, de-duplicated preserving first-requested order, and bounded at 20 distinct entries so the selection cannot degenerate into an unpaginated database read. get_task_counts does not offer ids -- it returns scalars, so selecting specific tasks there has no meaning, and omitting it from that schema lets closed-property validation reject it with a clear message. Plugin-side selection reuses the existing scoped pool and the shared filter predicate, so parent, project, and status semantics stay native; ordering and unresolved reporting happen client-side. Measured on a live database, 10 selected detail reads: 10 Bridge round trips to 1, elapsed 1042ms to 406ms (61% lower), identical items and identical field keys, so no broader data exposure. Validation impact: query, per focusrelay-dev classify. All semantic gates and the canary rotation pass; 272 tests pass including 26 new ones; direct MCP stdio probes cover ordering, unresolvedIDs, blank-ID, cursor, limit, and get_task_counts rejection, and confirm ordinary pagination still issues a query-bound cursor. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #172.
User outcome
An inbox workflow can read a compact page, pick the few ambiguous captures, and expand their notes in one bounded read instead of one
get_taskcall each. The observed cost this addresses: one inbox-zero UAT made 28 scalar detail calls for a 32-item inbox.Contract
list_tasks.filtergains an optionalidsarray:{ "filter": { "ids": ["id-1", "id-2"], "inboxOnly": true, "inboxView": "remaining" }, "fields": ["id", "name", "note"], "page": { "limit": 20 } }unresolvedIDs, never returned, and never replaced by an unrelated task.page.cursoris rejected,page.limitmust cover the requested IDs, and nonextCursoris issued.get_task_countsdoes not offerids. It returns scalars, so selecting specific tasks there has no meaning; omitting it from that schema lets closed-property validation reject it with a clear message rather than silently ignoring it.Plugin-side selection reuses the existing scoped pool and the shared filter predicate, so parent, project, and status semantics stay native and documented. Ordering and unresolved reporting happen client-side, keeping the plug-in change small.
Measured result
10 selected detail reads on a live database, one server process, same fields:
get_task× 10id, name, noteid, name, note10× fewer Bridge round trips, 61% lower elapsed time, identical items and identical field keys — so the reduction does not come with broader data exposure. The bounded response is larger in raw bytes only because it is one indented envelope rather than ten bare objects.
Validation
Impact:
query, perfocusrelay-dev classify.unresolvedIDswith no substitution; blank ID,cursor, and insufficientlimitare each rejected before Bridge dispatch;get_task_counts.filter.ids is unsupported; and ordinary paginatedlist_tasksstill issues a query-bound cursor.inboxOnly + remaining, and 0/5 with 5 unresolved undercompleted=true, with no unrelated tasks returned.One incidental change:
closingObjectSchemasbecame internal so wire tests can validate against the same closed schema clients receive, rather than a raw shape production never serves.Non-goals
Unchanged: name-based lookup, fuzzy matching, task caching, mutations, and
get_taskitself.🤖 Generated with Claude Code