fix: harden task listing against partial and out-of-range results - #120
Merged
Merged
Conversation
Follow-ups from #112. list_all_tasks silently returned partial results. When a pagination defense stopped the crawl early, callers got a short list with no way to tell it apart from a complete one. It now returns a TaskListing carrying the tasks plus a truncated flag, surfaced by the CLI in all three output formats and by the MCP tool as a `truncated` field. The loop also stops on any non-progressing page rather than only on an immediately repeated token, so a server cycling two or more tokens is caught on the first page that contributes nothing instead of spinning to the page cap. Drop the hardcoded 50-task default page size. page_size and history_length are proto3 optional fields with real presence, so leaving them unset genuinely omits them and the server applies its own default; the comment claiming otherwise was wrong. Verified against a live agent. Validation moves into shared validate_page_size and validate_history_length helpers, used by the service, CLI, and MCP instead of three copies. page_size now also rejects values above 100, the spec maximum, which a server would only reject after a round trip. history_length is validated on get_task as well, which had no check. Co-Authored-By: Claude Opus 5 (1M context) <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.
Follow-ups from #112, found while reviewing the merged ListTasks implementation.
Partial listings were indistinguishable from complete ones
list_all_taskshas three defenses against a misbehaving server, and all three ended the crawl by returning whatever had accumulated. A caller got a short list with no way to tell it apart from a genuinely complete one.It now returns a
TaskListing— the tasks plus atruncatedflag:Warning: listing stopped early; showing N task(s), which may not be all of them.--output json: atruncatedfield in the envelope--output ndjson: a trailing{"type": "truncated", "count": N}record, matching howerrorrecords already worklist_tasks: atruncatedfield alongsidecountThe token guard missed cycles longer than one
The loop only stopped when a server handed back the token just sent. A server alternating
A → B → Anever repeats consecutively, so it ran to the 1000-page cap. The loop now stops as soon as a page contributes no new task while still offering a token, which subsumes replayed pages and token cycles of any length.The hardcoded default page size was based on a wrong premise
The old comment said servers "commonly reject a zero page size and some treat an unset field as zero, so always send an explicit one," and sent 50 whenever the caller omitted it.
page_sizeandhistory_lengthare proto3optionalfields with real presence. Leaving them unset omits them from the wire entirely — it does not send zero. Confirmed against a live agent: an unset page size returns the full listing, while an explicit0is rejected withminimum page size is 1. The default is gone; omitting the flag now lets the server choose, which is what the docstring always claimed.Validation was duplicated and incomplete
Three copies of the page-size check lived in the service, the CLI, and the MCP tool. They are now
validate_page_sizeandvalidate_history_lengthincommon/input_validation.py, with alabelparameter so the CLI still names the flag the user typed whiledetails.fieldkeeps the machine name.Two gaps closed along the way:
page_sizeabove 100 is now rejected locally. The spec bounds a page to [1, 100]; previously a caller paid a round trip to learn that.get_tasknever validatedhistory_lengthat all.Testing
Full suite green (520 passed), plus ruff, format, and ty.
Exercised end to end against a live A2A agent (SDK
DefaultRequestHandlerV2+InMemoryTaskStore, JSON-RPC):task listwith and without--page-size, including real multi-page crawls — a paginated listing returns the same set as an unpaginated one0→minimum page size is 1,101→maximum page size is 100,100→ OK, unset → OK. The new local checks match the server exactly.truncated=True; uncapped returns 6 withtruncated=Falselist_tasks,get_task,send_messageagainst the same agent, plus all three validators rejecting correctlytask get,--statusfiltering,--history-length, and ndjson outputThe TUI path is unchanged by this PR and has not been manually exercised.
🤖 Generated with Claude Code