Skip to content

feat(agent): retry agent turn once on infra failure - #3145

Open
daryllimyt wants to merge 1 commit into
mainfrom
feat/agent-activity-infra-retry
Open

feat(agent): retry agent turn once on infra failure#3145
daryllimyt wants to merge 1 commit into
mainfrom
feat/agent-activity-infra-retry

Conversation

@daryllimyt

@daryllimyt daryllimyt commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Why

When the agent-executor pod was OOM-killed on 2026-07-27, all ten in-flight run_agent_activity executions heartbeat-timed-out with RETRY_STATE_MAXIMUM_ATTEMPTS_REACHED and every affected DurableAgentWorkflow (and parent workflow) failed — even though a replacement worker was polling 47 seconds before the first timeout fired. One retry would very likely have recovered all ten sessions.

What

A dedicated activity:agent_turn retry policy (maximum_attempts=2, same NON_RETRYABLE_ERROR_TYPES list) applied at both run_agent_activity call sites in DurableAgentWorkflow. Application/validation errors still fail on the first attempt — only infrastructure failures (heartbeat timeout, worker loss, timeouts) get the second attempt. The shared activity:fail_fast policy and its ~17 other call sites in the DSL and EE workflows are unchanged, with a regression test pinning both policies.

⚠️ Known limitation: the turn is not idempotent

Reviewed in depth before shipping; retries trade certain failure for rare duplication artifacts:

  • Existing-session retry: attempt 2 rehydrates the session from the DB (including attempt 1's partially persisted lines) and re-submits the same user prompt — persisted history can show the prompt twice, with attempt 1's partial transcript followed by attempt 2's full response. A tool side effect that completed before its result was persisted can be executed again.
  • First-turn retry: attempt 2's input still carries sdk_session_id=None, so it starts an independent SDK session; the Tracecat session history then contains both attempts' transcripts chronologically merged.
  • Line-UUID dedup is attempt-local (LoopbackHandler._persisted_line_uuids starts empty per attempt; no DB uniqueness constraint on the content UUID), so it does not protect across attempts — though the resumed-file offset means attempt 1's exact lines are normally not re-emitted.
  • A user watching the live stream sees attempt 1 stall, then attempt 2 restart output in the same turn.

Rationale for shipping anyway: the retry only fires on worker loss (rare, infra-caused), and the status quo is guaranteed failure of the session and its parent workflow. Follow-ups that would close the gap: reload session state at activity start for retries (mirror the approval-resume path), hydrate the SDK session ID on first-turn retries, and cross-attempt line dedup.

Testing

  • tests/unit/test_retry_policies.py: pins activity:agent_turn (attempts=2, non-retryable list identity) and activity:fail_fast (attempts=1); accessing the exact key guards the runtime-KeyError failure mode.
  • Audit: no existing test pinned the old agent-turn policy.
  • ruff, basedpyright --warnings, pytest on touched files: clean.

Summary by cubic

Add a one-time retry for agent turns to recover from infra failures like worker loss or heartbeat timeouts. This reduces failed sessions and parent workflows when a worker restarts mid-turn.

  • New Features
    • Added activity:agent_turn retry policy in tracecat.dsl.common (maximum_attempts=2, shares NON_RETRYABLE_ERROR_TYPES so app errors still fail fast).
    • Applied the policy to both run_agent_activity call sites in DurableAgentWorkflow; activity:fail_fast and other call sites remain unchanged.
    • Added tests/unit/test_retry_policies.py to pin both policies.
    • Limitation: retries are not idempotent and can occasionally duplicate persisted lines or re-run side effects.

Written for commit dfa2ec6. Summary will update on new commits.

Review in cubic

A worker loss (e.g. the recent agent-executor OOM kill) heartbeat-fails
every in-flight run_agent_activity with maximum_attempts=1, so all
affected sessions and their parent workflows fail outright even though
a healthy worker was polling before the timeouts fired.

Add a dedicated activity:agent_turn retry policy (maximum_attempts=2,
same non-retryable error list, so only infrastructure failures retry)
and use it at both run_agent_activity call sites. The shared
activity:fail_fast policy and all other call sites are unchanged.
@daryllimyt daryllimyt added enhancement New feature or request engine Improvements or additions to the workflow engine labels Jul 28, 2026
@zeropath-ai

zeropath-ai Bot commented Jul 28, 2026

Copy link
Copy Markdown

No security or compliance issues detected. Reviewed everything up to dfa2ec6.

Security Overview
Detected Code Changes
Change Type Relevant files
Enhancement ► packages/tracecat-ee/tracecat_ee/agent/workflows/durable.py
    Change retry_policy from RETRY_POLICIES["activity:fail_fast"] to RETRY_POLICIES["activity:agent_turn"]
► tests/unit/test_retry_policies.py
    Add tests for agent_turn retry policy and ensure fail_fast remains single attempt
► tracecat/dsl/common.py
    Introduce new retry policy "activity:agent_turn" with maximum_attempts=2 and NON_RETRYABLE_ERROR_TYPES

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dfa2ec624f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

),
heartbeat_timeout=timedelta(seconds=60),
retry_policy=RETRY_POLICIES["activity:fail_fast"],
retry_policy=RETRY_POLICIES["activity:agent_turn"],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Extend auth tokens across the retry window

When the first attempt fails late in a turn—especially on the configured start-to-close timeout—the retry reuses executor_input, including the original MCP and LLM JWTs. Both token types expire after TRACECAT__AGENT_SANDBOX_TIMEOUT + 60 seconds (tracecat/agent/tokens.py:174 and :366), while each activity attempt may run for the full sandbox timeout. Consequently, a retry after a start-to-close timeout begins with only about 60 seconds of token validity, and a retry after a late heartbeat timeout may start with an already-expired token, causing authenticated LLM/tool calls to fail instead of recovering. Mint tokens with enough lifetime for both attempts or refresh them per attempt before enabling this retry policy at both call sites.

Useful? React with 👍 / 👎.

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

Labels

engine Improvements or additions to the workflow engine enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant