Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/tracecat-ee/tracecat_ee/agent/workflows/durable.py
Original file line number Diff line number Diff line change
Expand Up @@ -1336,7 +1336,7 @@ async def _run_with_agent_executor(
seconds=config.TRACECAT__AGENT_SANDBOX_TIMEOUT
),
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 👍 / 👎.

)
else:
activity_handle = workflow.start_activity(
Expand All @@ -1348,7 +1348,7 @@ async def _run_with_agent_executor(
seconds=config.TRACECAT__AGENT_SANDBOX_TIMEOUT
),
heartbeat_timeout=timedelta(seconds=60),
retry_policy=RETRY_POLICIES["activity:fail_fast"],
retry_policy=RETRY_POLICIES["activity:agent_turn"],
)
# ActivityHandle is an asyncio.Task subclass, so .done() is
# valid. Neither wait_condition nor the handle poll emits
Expand Down
12 changes: 12 additions & 0 deletions tests/unit/test_retry_policies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from tracecat.dsl.common import NON_RETRYABLE_ERROR_TYPES, RETRY_POLICIES


def test_agent_turn_retry_policy() -> None:
policy = RETRY_POLICIES["activity:agent_turn"]

assert policy.maximum_attempts == 2
assert policy.non_retryable_error_types is NON_RETRYABLE_ERROR_TYPES


def test_fail_fast_retry_policy_remains_single_attempt() -> None:
assert RETRY_POLICIES["activity:fail_fast"].maximum_attempts == 1
6 changes: 6 additions & 0 deletions tracecat/dsl/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -1430,6 +1430,12 @@ def get_execution_type_from_search_attr(
maximum_attempts=1,
non_retryable_error_types=NON_RETRYABLE_ERROR_TYPES,
),
"activity:agent_turn": RetryPolicy(
# One retry for infrastructure failures (worker loss, heartbeat timeout).
# Application errors are excluded via NON_RETRYABLE_ERROR_TYPES.
maximum_attempts=2,
non_retryable_error_types=NON_RETRYABLE_ERROR_TYPES,
),
"activity:fail_slow": RetryPolicy(maximum_attempts=6),
"workflow:fail_fast": RetryPolicy(
# XXX: Do not set max attempts to 0, it will default to unlimited
Expand Down
Loading