Skip to content

Conversation

@kabir
Copy link
Collaborator

@kabir kabir commented Nov 13, 2025

I started working on the TCK tests a2aproject/a2a-tck#93 but then found changes were needed, and a lot of them are affected by the not-yet-merged a2aproject/A2A#1160.

  - Sort tasks by status.timestamp DESC (most recent first), then ID ASC
  - Add lastUpdatedAfter parameter filtering
  - Update InMemoryTaskStore and JpaDatabaseTaskStore sorting logic
  - Add timestamp denormalized column to JpaTask for efficient querying
  - Update all transport handlers (JSON-RPC, gRPC, REST)
@kabir kabir marked this pull request as draft November 13, 2025 15:20
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @kabir, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses necessary changes to enable the successful execution of new TCK tests for the tasks/list endpoint. The core of these changes involves a significant overhaul of the task listing and pagination mechanism, moving from a simple ID-based cursor to a more robust keyset pagination approach utilizing task timestamps. Additionally, a new lastUpdatedAfter filter has been introduced to enhance query capabilities. These updates ensure compliance with the latest specification requirements, including a temporary protobuf schema divergence to align with an anticipated upstream change.

Highlights

  • Enhanced Task Listing Pagination: Implemented keyset pagination for ListTasks endpoints, sorting tasks by statusTimestamp (descending) and then id (ascending) for consistent and efficient retrieval. This replaces the previous ID-based cursor pagination.
  • New lastUpdatedAfter Filter: Added a new parameter to the ListTasks API to filter tasks based on their statusTimestamp, allowing retrieval of tasks updated after a specific point in time. This includes validation to prevent future timestamps.
  • Page Token Format Update: The pageToken now uses a composite format of 'timestamp_millis:taskId' to support keyset pagination, with robust validation for correct format and error handling for malformed tokens.
  • Denormalized statusTimestamp: Introduced a statusTimestamp field in JpaTask to denormalize the task's last update timestamp, enabling efficient database indexing and querying for the new sorting and filtering mechanisms.
  • Improved History Limiting Logic: Refined the historyLength parameter logic to explicitly handle historyLength=0 by returning an empty history list, ensuring clearer behavior.
  • Protobuf Schema Alignment: Updated the ListTasksResponse protobuf definition to include a page_size field and reordered total_size to align with an upcoming upstream PR (#1160). A PROTO_DIVERGENCE.md file has been added to document this temporary divergence.
  • Comprehensive Test Coverage: Added new unit tests to JpaDatabaseTaskStoreTest to validate the new keyset pagination logic, including scenarios with different timestamps and invalid page token formats, ensuring robustness.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces significant changes to the task listing functionality to align with new TCK tests. The key changes include implementing keyset pagination based on a composite sort key (timestamp and ID), adding a lastUpdatedAfter filter, and ensuring consistent behavior across both JPA and in-memory task stores. The protobuf specification has also been updated to include pageSize in the ListTasksResponse.

The changes are well-tested, with new unit tests covering the new pagination and filtering logic. My review focuses on improving code maintainability by reducing duplication, fixing a potential null pointer exception in the in-memory implementation, and removing temporary debug logging.

Comment on lines +100 to +110
java.time.Instant taskTimestamp = task.getStatus().timestamp().toInstant()
.truncatedTo(java.time.temporal.ChronoUnit.MILLIS);
int timestampCompare = taskTimestamp.compareTo(tokenTimestamp);

if (timestampCompare < 0 || (timestampCompare == 0 && task.getId().compareTo(tokenId) > 0)) {
// This task is after the token, search left half
right = mid;
} else {
// This task is before or equal to token, search right half
left = mid + 1;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

There is a potential NullPointerException here. If a Task has a null status, task.getStatus() will be null, and calling .timestamp() on it will cause an NPE. The filtering logic allows tasks with null status to be included in allFilteredTasks if params.status() is not specified. The sorting logic correctly handles this by placing them at the end, but this binary search does not. You should add a null check for task.getStatus() before accessing its properties.

                        java.time.Instant taskTimestamp = (task.getStatus() != null && task.getStatus().timestamp() != null)
                                ? task.getStatus().timestamp().toInstant().truncatedTo(java.time.temporal.ChronoUnit.MILLIS)
                                : null;

                        if (taskTimestamp == null) {
                            // Task with null timestamp is always "before" a token with a timestamp, as they are sorted last.
                            // So, we search in the right half.
                            left = mid + 1;
                        } else {
                            int timestampCompare = taskTimestamp.compareTo(tokenTimestamp);

                            if (timestampCompare < 0 || (timestampCompare == 0 && task.getId().compareTo(tokenId) > 0)) {
                                // This task is after the token, search left half
                                right = mid;
                            } else {
                                // This task is before or equal to token, search right half
                                left = mid + 1;
                            }
                        }

Comment on lines 238 to 249
if (params.pageToken() != null && !params.pageToken().isEmpty()) {
queryBuilder.append(" AND t.id > :pageToken");
String[] tokenParts = params.pageToken().split(":", 2);
if (tokenParts.length == 2) {
// Keyset pagination: get tasks where timestamp < tokenTimestamp OR (timestamp = tokenTimestamp AND id > tokenId)
// All tasks have timestamps (TaskStatus canonical constructor ensures this)
queryBuilder.append(" AND (t.statusTimestamp < :tokenTimestamp OR (t.statusTimestamp = :tokenTimestamp AND t.id > :tokenId))");
} else {
// Legacy ID-only pageToken format is not supported with timestamp-based sorting
// Throw error to prevent incorrect pagination results
throw new io.a2a.spec.InvalidParamsError(null, "Invalid pageToken format: expected 'timestamp:id'", null);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The logic for parsing and validating the pageToken is duplicated here and in the block for setting query parameters (lines 267-286). This increases the maintenance burden and risk of inconsistencies. Consider refactoring to parse the token once at the beginning of the method, store the timestamp and ID in local variables, and then reuse them for building the query and setting parameters. This would centralize the validation and parsing logic.

Comment on lines +46 to +51
// DEBUG: Log store contents for totalSize investigation
LOGGER.debug("=== InMemoryTaskStore.list() DEBUG ===");
LOGGER.debug("Total tasks in store: {}", tasks.size());
LOGGER.debug("Filter contextId: {}", params.contextId());
LOGGER.debug("Filter status: {}", params.status());
tasks.values().forEach(t -> LOGGER.debug(" Task: id={}, contextId={}", t.getId(), t.getContextId()));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

These detailed debug logs seem to have been added for troubleshooting during development. While useful, they are quite verbose and could clutter the logs. It's good practice to remove such temporary debugging statements before merging the pull request.

Comment on lines +63 to +66
(Task t) -> (t.getStatus() != null && t.getStatus().timestamp() != null)
// Truncate to milliseconds for consistency with pageToken precision
? t.getStatus().timestamp().toInstant().truncatedTo(java.time.temporal.ChronoUnit.MILLIS)
: null,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The null check for t.getStatus().timestamp() is redundant. The canonical constructor of TaskStatus ensures that the timestamp is never null, defaulting to OffsetDateTime.now(ZoneOffset.UTC) if one isn't provided. You can simplify this to just check t.getStatus() != null.

Suggested change
(Task t) -> (t.getStatus() != null && t.getStatus().timestamp() != null)
// Truncate to milliseconds for consistency with pageToken precision
? t.getStatus().timestamp().toInstant().truncatedTo(java.time.temporal.ChronoUnit.MILLIS)
: null,
(Task t) -> (t.getStatus() != null)
// Truncate to milliseconds for consistency with pageToken precision
? t.getStatus().timestamp().toInstant().truncatedTo(java.time.temporal.ChronoUnit.MILLIS)
: null,

Comment on lines +162 to +165
LOGGER.debug("=== ProtoUtils.ToProto.listTasksResult() DEBUG ===");
LOGGER.debug("Input result.totalSize(): {}", result.totalSize());
LOGGER.debug("Input result.pageSize(): {}", result.pageSize());
LOGGER.debug("Input result.tasks().size(): {}", result.tasks().size());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

These detailed debug logs seem to have been added for troubleshooting. It's good practice to remove temporary debugging statements like these before merging to keep the codebase clean and avoid log clutter, even at the DEBUG level.

@fjuma
Copy link
Collaborator

fjuma commented Nov 24, 2025

This one now has some conflicts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants