Repository navigation
Conversation
🧪 Code Coverage (vs
|
| Base | PR | Delta | |
|---|---|---|---|
| src/a2a/server/agent_execution/active_task.py | 95.40% | 95.45% | 🟢 +0.05% |
| src/a2a/server/cluster/database_event_stream.py | 92.86% | 96.94% | 🟢 +4.08% |
| src/a2a/server/tasks/task_manager.py | 98.86% | 98.92% | 🟢 +0.06% |
| Total | 92.89% | 92.95% | 🟢 +0.05% |
Generated by coverage-comment.yml
Framework failure paths could persist a FAILED task without status.timestamp: the consumer failure path built a TaskStatusUpdateEvent with an unstamped TaskStatus (which TaskManager.save_task_event then CopyFrom'd over the task, clearing any prior timestamp), and the producer failure path mutated task.status.state directly before saving. DatabaseTaskStore derives last_updated from status.timestamp, so the task was persisted with last_updated = NULL, breaking ListTasks ordering and time-based filters (issue a2aproject#1331). Per the approach endorsed on the issue, this lands both layers: - failure sites in ActiveTask (_write_failed_status, _mark_task_as_failed, and the producer failure path) stamp the current time explicitly; - TaskManager.save_task_event gains a defensive backstop that auto-populates the timestamp whenever a terminal state arrives unstamped, covering raw Task and status-event paths alike. Non-terminal states are left untouched, and explicitly provided timestamps are never overwritten. Signed-off-by: andy <linux2011@qq.com>
c34c53f to
6be10db
Compare
|
Friendly ping — this is ready for review whenever anyone has bandwidth (both branches just rebased onto latest |
mykytanetipa
left a comment
There was a problem hiding this comment.
Why only stamp terminal states in TaskManager?
-
If an agent sends an unstamped WORKING or INPUT_REQUIRED update (for example via new_text_status_update_event, which doesn't set a timestamp), task.status.CopyFrom(event.status) still wipes out the existing timestamp and writes last_updated = NULL to the DB.
-
New SUBMITTED tasks created via new_task_from_user_message or TaskManager._init_task_obj (which ensure_task_id saves directly through _save_task) also start without a timestamp, so tasks created with return_immediately=True or artifact-first flows end up with last_updated = NULL too.
I think we should stamp any status that lacks a timestamp (in save_task_event and _init_task_obj / _save_task), not just terminal ones
| status=TaskStatus(state=TaskState.TASK_STATE_FAILED), | ||
| status=TaskStatus( | ||
| state=TaskState.TASK_STATE_FAILED, | ||
| timestamp=_now_timestamp(), |
There was a problem hiding this comment.
This needs a rebase onto main. #1366 already removed the failure write in _run_producer and updated _write_failed_status to set status.timestamp.FromDatetime(datetime.now(timezone.utc)) directly.
After rebasing, only _mark_task_as_failed (and cancel, see below) will need stamping here, and we can just call .FromDatetime(datetime.now(timezone.utc)) directly instead of adding _now_timestamp().
| updated = Task() | ||
| updated.CopyFrom(task) | ||
| updated.status.state = TaskState.TASK_STATE_CANCELED |
There was a problem hiding this comment.
We are still missing timestamps on the CANCELED paths:
-
In ActiveTask.cancel(), updated.CopyFrom(task) copies the old timestamp from the previous state (like WORKING or INPUT_REQUIRED) and only changes state to CANCELED. Since timestamp is already set on the copy, _stamp_terminal_timestamp in TaskManager won't update it, so the task keeps the stale timestamp.
-
In DefaultRequestHandlerV2._write_cancel(), we call _versioned_store.save() directly without going through TaskManager, so cancelled.status.timestamp is either left empty or keeps the old timestamp.
Both places should explicitly set status.timestamp.FromDatetime(datetime.now(timezone.utc)) when transitioning to CANCELED.
| task.status.CopyFrom(event.status) | ||
| self._stamp_terminal_timestamp(task) |
There was a problem hiding this comment.
If we stamp task after task.status.CopyFrom(event.status), event.status.timestamp stays empty. That same event object is then sent to streaming subscribers, push notifications, and the task_events table without a timestamp. If we stamp event.status before CopyFrom, both the event and the task get the same timestamp.
Description
Framework failure paths could persist a FAILED task without
status.timestamp, andDatabaseTaskStorederiveslast_updatedfromstatus.timestamp(database_task_store.py:152-156) — so the task was persisted withlast_updated = NULL, breakingListTasksordering andstatus_timestamp_afterfiltering. Root-cause analysis and a runtime reproduction are in #1331.Per the two-layer approach endorsed on the issue:
ActiveTask):_write_failed_status,_mark_task_as_failed, and the producer failure path now stamp the current time on the FAILED status.TaskManager.save_task_event: a terminal state arriving without a timestamp (rawTaskpath or status-event path) is auto-stamped with the current time, so future call sites cannot reintroduce the NULL-last_updatedhole.Semantics: non-terminal states are never touched, and explicitly provided timestamps are never overwritten.
fix:→ SemVer patch)Fixes #1331 🦕
Validation
TASK_STATE_FAILEDwithstatus.timestampset; non-terminal statuses and explicitly provided timestamps are untouched.state=TASK_STATE_FAILED, timestamp set=False(repro in [Bug]: Framework-written FAILED status drops status.timestamp, persisting last_updated as NULL #1331). AFTER:timestamp set=True.tests/server/tasks/test_task_manager.py(terminal-unstamped auto-stamp, provided-timestamp preserved, non-terminal untouched, raw-Task path guarded) + 1 end-to-end intest_default_request_handler_v2.py.2302 passed, coverage93%(gate 88%).Prepared with AI assistance (WorkBuddy); reviewed and verified by Linux2010