Skip to content

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

Description

@Linux2010

What happened?

When the framework itself writes a terminal FAILED state (consumer failure, producer failure), the resulting TaskStatus carries no timestamp. Because TaskManager.save_task_event replaces the status wholesale via task.status.CopyFrom(event.status) (src/a2a/server/tasks/task_manager.py:265), the previously stamped status.timestamp is cleared, and DatabaseTaskStore.save derives last_updated from status.timestamp (src/a2a/server/tasks/database_task_store.py:152-156) — persisting NULL.

Consequences:

  • ListTasks ordering (which sorts on last_updated) drops the task to the end;
  • the status_timestamp_after filter never matches the task again;
  • API responses lose the status.timestamp field entirely.

The behavior is also internally inconsistent: the cancel-failure path _mark_task_as_failed (src/a2a/server/agent_execution/active_task.py:945-963) copies the existing status and only overwrites state (preserving the timestamp), while the consumer failure path _write_failed_status (active_task.py:161-169) and the producer failure path construct a fresh TaskStatus(state=...) with no timestamp.

Reproduction

Self-contained script (main @ 494a8ec, Python 3.10)
import asyncio

from a2a.auth.user import UnauthenticatedUser
from a2a.helpers.proto_helpers import new_task_from_user_message
from a2a.server.agent_execution import AgentExecutor
from a2a.server.context import ServerCallContext
from a2a.server.events import EventQueue
from a2a.server.request_handlers import DefaultRequestHandlerV2
from a2a.server.tasks import InMemoryTaskStore
from a2a.types.a2a_pb2 import (
    AgentCapabilities,
    AgentCard,
    Message,
    Part,
    Role,
    SendMessageConfiguration,
    SendMessageRequest,
    TaskState,
)


class ExplodingExecutor(AgentExecutor):
    task_id = None

    async def execute(self, context, event_queue):
        task = new_task_from_user_message(context.message)
        self.task_id = task.id
        await event_queue.enqueue_event(task)
        raise RuntimeError('agent crashed mid-flight')

    async def cancel(self, context, event_queue):
        pass


async def main():
    task_store = InMemoryTaskStore()
    executor = ExplodingExecutor()
    handler = DefaultRequestHandlerV2(
        agent_executor=executor,
        task_store=task_store,
        agent_card=AgentCard(
            name='t',
            version='1.0',
            capabilities=AgentCapabilities(streaming=True),
        ),
    )
    params = SendMessageRequest(
        message=Message(
            role=Role.ROLE_USER, message_id='m1', parts=[Part(text='Hi')]
        ),
        configuration=SendMessageConfiguration(
            accepted_output_modes=['text/plain']
        ),
    )
    try:
        await handler.on_message_send(
            params, ServerCallContext(user=UnauthenticatedUser())
        )
    except Exception as e:
        print(f'on_message_send raised {type(e).__name__} (expected)')
    task = await task_store.get(
        executor.task_id, ServerCallContext(user=UnauthenticatedUser())
    )
    print(
        f'persisted: state={TaskState.Name(task.status.state)}, '
        f'timestamp set={task.status.HasField("timestamp")}'
    )


asyncio.run(main())

Observed output:

on_message_send raised RuntimeError (expected)
persisted: state=TASK_STATE_FAILED, timestamp set=False

Expected behavior

Every task state transition the framework writes — including failures — should carry a status.timestamp, matching what TaskUpdater.update_status does for agent-driven transitions.

Suggested fix

Either:

  • stamp the current time in the three framework failure paths (_write_failed_status, the producer failure path, and — for consistency — _mark_task_as_failed), or
  • in TaskManager.save_task_event, preserve the previous status.timestamp when the incoming status event has none.

Happy to submit a PR with tests.

Related

Observed as a secondary effect in the reproduction of #1313.

Code of Conduct

  • I agree to follow this project's Code of Conduct

Activity

  1. added
    component: serverIssues related to frameworks for agent execution, HTTP/event handling, database persistence logic.
    on Oct 14, 2026
  2. added theissue type on Oct 14, 2026
  3. rohityan commented on Oct 15, 2026

    @rohityan

    Hi @Linux2010 , thanks for reporting this. I was able to reproduce this issue.

    When an unhandled exception is raised during agent execution, the exception is caught within ActiveTask._run_producer. In this error-handling path, the framework transitions the task to TaskState.TASK_STATE_FAILED directly on the retrieved task instance and passes it to TaskManager.save_task_event. Because task.status.state is mutated without stamping task.status.timestamp, and because TaskManager.save_task_event persists raw Taskevents directly via TaskManager._save_task without generating or merging timestamp metadata as is done for TaskStatusUpdateEvent, task.status.timestamp remains unset. Consequently, DatabaseTaskStore._to_orm evaluates task.status.HasField('timestamp') as False, persisting last_updated as NULL and breaking downstream queries, sorting, and time-based filtering.

    Between the two options you proposed, Option 1 (stamping current time at the failure sites) is the right approach. However, option 1 alone is not fully comprehensive because it only patches known caller sites, leaving the database vulnerable if other code paths or raw Task / TaskStatusUpdateEvent instances emit a state transition without a timestamp. To be truly robust, it must be paired with a defensive guard in TaskManager.save_task_event that auto-populates timestamp with current time whenever any terminal state arrives unstamped.

    Please feel free to contribute and link a PR. Assigning this issue to you till your linked PR gets reviewed.

  4. Linux2010 commented on Oct 16, 2026

    @Linux2010
    ContributorAuthor

    PR submitted: #1346

    Implements both layers as endorsed: the three failure sites in ActiveTask now stamp the current time explicitly, and TaskManager.save_task_event gained the defensive backstop that auto-populates the timestamp whenever a terminal state arrives unstamped (raw Task and status-event paths both covered; non-terminal states and explicitly provided timestamps untouched). Tests cover all four behaviors plus the end-to-end persistence from the issue's reproduction.

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

Metadata

Metadata

Assignees

Labels

component: serverIssues related to frameworks for agent execution, HTTP/event handling, database persistence logic.

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions