Skip to content

fix(server): always stamp status.timestamp on terminal task states - #1346

Open
Linux2010 wants to merge 1 commit into
a2aproject:mainfrom
Linux2010:fix/1331-failed-timestamp
Open

Linux2010 wants to merge 1 commit into
a2aproject:mainfrom
Linux2010:fix/1331-failed-timestamp

Conversation

@Linux2010

Copy link
Copy Markdown
Contributor

Description

Framework failure paths could persist a FAILED task without status.timestamp, and DatabaseTaskStore derives last_updated from status.timestamp (database_task_store.py:152-156) — so the task was persisted with last_updated = NULL, breaking ListTasks ordering and status_timestamp_after filtering. Root-cause analysis and a runtime reproduction are in #1331.

Per the two-layer approach endorsed on the issue:

  • Failure sites stamp explicitly (ActiveTask): _write_failed_status, _mark_task_as_failed, and the producer failure path now stamp the current time on the FAILED status.
  • Defensive backstop in TaskManager.save_task_event: a terminal state arriving without a timestamp (raw Task path or status-event path) is auto-stamped with the current time, so future call sites cannot reintroduce the NULL-last_updated hole.

Semantics: non-terminal states are never touched, and explicitly provided timestamps are never overwritten.

  • Follow the CONTRIBUTING Guide
  • PR title follows Conventional Commits (fix: → SemVer patch)
  • Tests and linter pass
  • Appropriate docs were updated (guard docstring states the contract)

Fixes #1331 🦕

Validation

  • Claim: an executor crashing mid-flight now persists TASK_STATE_FAILED with status.timestamp set; non-terminal statuses and explicitly provided timestamps are untouched.
  • Exact head: c34c53f
  • BEFORE: persisted task = 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: 4 new tests in 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 in test_default_request_handler_v2.py.
  • Full suite: 2302 passed, coverage 93% (gate 88%).
  • Environment: Python 3.10.20, macOS.
  • Limits / not tested: DatabaseTaskStore integration (covered indirectly via the same status pipeline; the e2e asserts the in-memory store the same way CI's coverage report measures it).

Prepared with AI assistance (WorkBuddy); reviewed and verified by Linux2010

@github-actions

github-actions Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

🧪 Code Coverage (vs main)

⬇️ Download Full Report

Base PR Delta
src/a2a/server/agent_execution/active_task.py 96.10% 96.11% 🟢 +0.01%
src/a2a/server/cluster/database_event_stream.py 93.64% 97.27% 🟢 +3.64%
src/a2a/server/request_handlers/default_request_handler_v2.py 88.98% 89.01% 🟢 +0.03%
src/a2a/server/tasks/task_manager.py 98.95% 98.98% 🟢 +0.04%
Total 93.42% 93.46% 🟢 +0.04%

Generated by coverage-comment.yml

@mykytanetipa mykytanetipa self-assigned this Oct 19, 2026
@Linux2010
Linux2010 force-pushed the fix/1331-failed-timestamp branch from c34c53f to 6be10db Compare October 21, 2026 02:00
@Linux2010

Copy link
Copy Markdown
Contributor Author

Friendly ping — this is ready for review whenever anyone has bandwidth (both branches just rebased onto latest main). Happy to address any feedback. Thanks!

@mykytanetipa mykytanetipa left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

Comment thread src/a2a/server/tasks/task_manager.py Resolved
Linux2010 added a commit to Linux2010/a2a-python that referenced this pull request Nov 2, 2026
Most task-lifecycle defects in this SDK are not wrong values: they are a
correct value that reached one surface and not the others (persisted store,
streaming subscribers, push notifications, the cluster task_events table,
and the two separate v0.3 compat mappers). The existing rule layer routes
mistakes to mistake-reflection and code style to docs/ai, but nothing tells
a change agent which surfaces a status write must reach, so the same class
keeps recurring: a2aproject#1276 (status.timestamp mapped for JSON-RPC/REST compat but
not gRPC), a2aproject#1188 (handler resumes from a stale snapshot), a2aproject#1206/a2aproject#1411
(contextId inference disagrees between events and the stored task), and the
review findings on a2aproject#1346 (stamping the copy instead of the event, and missing
the CANCELED paths).

Adds .agents/skills/task-state-invariants/SKILL.md with the invariant set
(each claim anchored to current main), the sibling-surface checklist, the
matching test files per surface, and the evidence expected in a PR, plus a
one-line router pointer in AGENTS.md so agents actually load it.

Signed-off-by: andy <linux2011@qq.com>
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>
@Linux2010
Linux2010 force-pushed the fix/1331-failed-timestamp branch from 6be10db to c923c37 Compare November 3, 2026 16:21
@Linux2010

Linux2010 commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor Author

All review threads here are addressed and resolved (rebased + fixes pushed as c923c37). Thanks for the detailed review — the CANCELED paths and the event-vs-copy stamping order were the two things I had missed.

Summary of what changed after your review:

  • rebased onto current main; _write_failed_status and the producer path now defer to fix(server): record executor failures through the event consumer #1366, and the _now_timestamp() helper is gone in favor of direct datetime.now(timezone.utc) calls;
  • ActiveTask.cancel() and DefaultRequestHandlerV2._write_cancel() now stamp CANCELED explicitly (CopyFrom was carrying the previous state's timestamp);
  • the TaskManager backstop stamps event.status before CopyFrom, so subscribers, push notifications and the task_events table get the timestamp too, not just the stored task;
  • TERMINAL_TASK_STATES is imported from a2a.utils.constants rather than re-declared locally;
  • new test test_save_task_event_stamps_the_published_event pins the event/task timestamp equality.

Local: ruff + ty clean, 2526 passed, coverage 94%. The 4 cross_version integration errors are environmental (identical on a clean main checkout in this sandbox).

This branch has not been deployed

No deployments
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.

[Bug]: Framework-written FAILED status drops status.timestamp, persisting last_updated as NULL

2 participants