Skip to content

Commit 5ad81e8

Browse files
authored
feat(retention): strict bool env parsing + stale-RUNNING cleanup override (#306)
Two hardening changes to the scheduled task-retention cleanup (#272 follow-up) ahead of the EY rollout. Context: [EY data-retention thread](https://scaleapi.slack.com/archives/C0AMZ12G2G7) + the sgp-dev verification doc. ## 1. Strict boolean env parsing `RETENTION_CLEANUP_DRY_RUN` was parsed with `os.environ.get(...) == "true"`, so **any** unrecognized value silently meant `dry_run=False` → live deletion. `DRY_RUN=True` (capital T — exactly what YAML/Helm tooling tends to render) was the documented gotcha in the sgp-dev test writeup; in the EY environment this config flows through system-manager value_overrides → Helm → env, which makes that footgun very real. Booleans now accept `true/false/1/0` case-insensitively and **raise on anything else**, so a misconfigured worker refuses to run instead of deleting data. Applied to `RETENTION_CLEANUP_DRY_RUN`, `RETENTION_CLEANUP_ENABLED`, and `ENABLE_HEALTH_CHECK_WORKFLOW` (same pattern). ## 2. `RETENTION_CLEANUP_STALE_RUNNING_DAYS` (default `0` = disabled, current behavior unchanged) The sweep unconditionally refuses RUNNING tasks. Tasks abandoned mid-run (agent crashed, workflow never reached terminal state) therefore stay in Mongo/Postgres **forever** — the sgp-dev test surfaced 121 such tasks on a single agent (`test-v2`: cleaned 0, skipped 121). For a retention-compliance feature that's a structural gap: the data most likely to be forgotten is precisely the data the policy can never touch. When set `> 0`, a RUNNING task with no interaction for at least that many days (same last-interaction definition as the idle check: `max(task.updated_at, latest message)`) is treated as abandoned and becomes cleanable. Each override emits a WARNING forensic log. The unprocessed-events correctness guard is unchanged. Recommended setting for OneEdge/EY: `30`. Wiring: env → `load_cleanup_config` → sweep → child workflow → `clean_task` activity → use case → service. The new activity parameter defaults to `0`, so in-flight workflows started before a deploy are unaffected. ## Tests - env parsing: case-insensitivity matrix, garbage → `ValueError`, new var default/parse - service: RUNNING refused by default; stale-RUNNING cleaned with override; idle-but-not-stale-enough refused; recent Mongo message blocks override; preview honors override without writes - workflow: `stale_running_days` propagation sweep → child → activity; existing fakes updated for the new activity arg - `uv run --group test pytest tests/unit/...retention...` → **36 passed**; ruff + ruff-format clean cc @stas <!-- greptile_comment --> <h3>Greptile Summary</h3> This PR hardens retention cleanup configuration and adds an override for abandoned RUNNING tasks. The main changes are: - Strict parsing for boolean env vars using `true/false/1/0` case-insensitively. - New `RETENTION_CLEANUP_STALE_RUNNING_DAYS` setting for stale RUNNING task cleanup. - Retention service guard updates that compute last interaction once and scope event-guard relaxation to stale RUNNING tasks. - Temporal activity and workflow wiring for the new stale-running setting. - Unit coverage for env parsing, service guard behavior, and workflow propagation. <details><summary><h3>Confidence Score: 5/5</h3></summary> The retention cleanup changes appear merge-safe with targeted coverage across configuration parsing, service guard behavior, and workflow propagation. The implementation is narrowly scoped, preserves the default behavior for stale RUNNING cleanup, fails closed on invalid boolean configuration, and includes unit tests covering the key runtime paths. </details> <details><summary><h3><a href="https://www.greptile.com/trex"><img alt="T-Rex" src="https://greptile-static-assets.s3.amazonaws.com/trex/trex_green.svg" height="20" align="absmiddle"></a> T-Rex Logs</h3></summary> **What T-Rex did** - An env boolean-parsing test was run to compare pre-change and post-change behavior: before, boolean env vars were silently coerced to false; after, 'True' and '1' are true, 'False' and '0' are false, 'garbage' raises ValueError, and stale\_running\_days is returned as 0 by default or 14 when overridden. - A RUNNING cleanup workflow test was executed to verify stale\_running\_days handling, including the default 0 and an explicit 9, ensuring that RUNNING is refused in certain cases, idle cleanup runs with delete/update counters, zero writes are produced in dry-run or recent-update scenarios, and that stale\_running\_days is included in child/activity arguments. <a href="https://app.greptile.com/trex/runs/12291157/artifacts"><picture><source media="(prefers-color-scheme: dark)" srcset="https://greptile-static-assets.s3.amazonaws.com/badges/ViewAllArtifactsDark.svg?v=1"><source media="(prefers-color-scheme: light)" srcset="https://greptile-static-assets.s3.amazonaws.com/badges/ViewAllArtifacts.svg?v=1"><img alt="View all artifacts" src="https://greptile-static-assets.s3.amazonaws.com/badges/ViewAllArtifacts.svg?v=1" height="32"></picture></a> <sub><a href="https://www.greptile.com/trex"><img alt="T-Rex" src="https://greptile-static-assets.s3.amazonaws.com/trex/trex_green.svg" height="14" align="absmiddle"></a> Ran code and verified through T-Rex</sub> </details> <sub>Reviews (5): Last reviewed commit: ["Merge branch &#39;main&#39; into retention/stale..."](5c0abc4) | [Re-trigger Greptile](https://app.greptile.com/api/retrigger?id=37220328)</sub> <!-- /greptile_comment -->
1 parent b43e8c3 commit 5ad81e8

10 files changed

Lines changed: 507 additions & 42 deletions

agentex/docker-compose.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ services:
177177
- RETENTION_CLEANUP_PAGE_SIZE=${RETENTION_CLEANUP_PAGE_SIZE:-200}
178178
- RETENTION_CLEANUP_MAX_IN_FLIGHT=${RETENTION_CLEANUP_MAX_IN_FLIGHT:-20}
179179
- RETENTION_CLEANUP_DRY_RUN=${RETENTION_CLEANUP_DRY_RUN:-true}
180+
- RETENTION_CLEANUP_STALE_RUNNING_DAYS=${RETENTION_CLEANUP_STALE_RUNNING_DAYS:-0}
180181
ports:
181182
- "5003:5003"
182183
volumes:
@@ -242,6 +243,7 @@ services:
242243
- RETENTION_CLEANUP_PAGE_SIZE=${RETENTION_CLEANUP_PAGE_SIZE:-200}
243244
- RETENTION_CLEANUP_MAX_IN_FLIGHT=${RETENTION_CLEANUP_MAX_IN_FLIGHT:-20}
244245
- RETENTION_CLEANUP_DRY_RUN=${RETENTION_CLEANUP_DRY_RUN:-true}
246+
- RETENTION_CLEANUP_STALE_RUNNING_DAYS=${RETENTION_CLEANUP_STALE_RUNNING_DAYS:-0}
245247
volumes:
246248
- .:/app:cached
247249
depends_on:

agentex/src/config/environment_variables.py

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ class EnvVarKeys(str, Enum):
6565
RETENTION_CLEANUP_PAGE_SIZE = "RETENTION_CLEANUP_PAGE_SIZE"
6666
RETENTION_CLEANUP_MAX_IN_FLIGHT = "RETENTION_CLEANUP_MAX_IN_FLIGHT"
6767
RETENTION_CLEANUP_DRY_RUN = "RETENTION_CLEANUP_DRY_RUN"
68+
RETENTION_CLEANUP_STALE_RUNNING_DAYS = "RETENTION_CLEANUP_STALE_RUNNING_DAYS"
6869

6970

7071
class Environment(str, Enum):
@@ -76,6 +77,30 @@ class Environment(str, Enum):
7677
refreshed_environment_variables = None
7778

7879

80+
def _parse_bool_env(key: EnvVarKeys, default: bool) -> bool:
81+
"""
82+
Strict boolean env parsing: accepts true/false/1/0 case-insensitively,
83+
raises on anything else.
84+
85+
The previous pattern (`os.environ.get(key, ...) == "true"`) silently
86+
coerced any unrecognized value to False. For RETENTION_CLEANUP_DRY_RUN
87+
that failure mode is destructive: `DRY_RUN=True` (capital T, as YAML
88+
tooling tends to render booleans) meant dry_run=False, i.e. live
89+
deletion. Fail loud instead so a misconfigured worker refuses to run.
90+
"""
91+
raw = os.environ.get(key)
92+
if raw is None:
93+
return default
94+
normalized = raw.strip().lower()
95+
if normalized in ("true", "1"):
96+
return True
97+
if normalized in ("false", "0"):
98+
return False
99+
raise ValueError(
100+
f"Invalid boolean for {key.value}: {raw!r} (expected true/false/1/0)"
101+
)
102+
103+
79104
class EnvironmentVariables(BaseModel):
80105
ENVIRONMENT: str | None = Environment.DEV
81106
OPENAI_API_KEY: str | None
@@ -128,6 +153,10 @@ class EnvironmentVariables(BaseModel):
128153
RETENTION_CLEANUP_PAGE_SIZE: int = 200
129154
RETENTION_CLEANUP_MAX_IN_FLIGHT: int = 20
130155
RETENTION_CLEANUP_DRY_RUN: bool = True
156+
# When > 0, tasks stuck in RUNNING with no interaction for this many days
157+
# are treated as abandoned and become eligible for cleanup. 0 disables the
158+
# override (RUNNING tasks are never cleaned), preserving prior behavior.
159+
RETENTION_CLEANUP_STALE_RUNNING_DAYS: int = 0
131160

132161
@classmethod
133162
def refresh(cls, force_refresh: bool = False) -> EnvironmentVariables | None:
@@ -210,15 +239,14 @@ def refresh(cls, force_refresh: bool = False) -> EnvironmentVariables | None:
210239
AGENTEX_SERVER_TASK_QUEUE=os.environ.get(
211240
EnvVarKeys.AGENTEX_SERVER_TASK_QUEUE
212241
),
213-
ENABLE_HEALTH_CHECK_WORKFLOW=(
214-
os.environ.get(EnvVarKeys.ENABLE_HEALTH_CHECK_WORKFLOW, "false")
215-
== "true"
242+
ENABLE_HEALTH_CHECK_WORKFLOW=_parse_bool_env(
243+
EnvVarKeys.ENABLE_HEALTH_CHECK_WORKFLOW, default=False
216244
),
217245
WEBHOOK_REQUEST_TIMEOUT=float(
218246
os.environ.get(EnvVarKeys.WEBHOOK_REQUEST_TIMEOUT, "15.0")
219247
),
220-
RETENTION_CLEANUP_ENABLED=(
221-
os.environ.get(EnvVarKeys.RETENTION_CLEANUP_ENABLED, "false") == "true"
248+
RETENTION_CLEANUP_ENABLED=_parse_bool_env(
249+
EnvVarKeys.RETENTION_CLEANUP_ENABLED, default=False
222250
),
223251
RETENTION_CLEANUP_AGENT_ALLOWLIST=[
224252
name.strip()
@@ -239,8 +267,11 @@ def refresh(cls, force_refresh: bool = False) -> EnvironmentVariables | None:
239267
RETENTION_CLEANUP_MAX_IN_FLIGHT=int(
240268
os.environ.get(EnvVarKeys.RETENTION_CLEANUP_MAX_IN_FLIGHT, "20")
241269
),
242-
RETENTION_CLEANUP_DRY_RUN=(
243-
os.environ.get(EnvVarKeys.RETENTION_CLEANUP_DRY_RUN, "true") == "true"
270+
RETENTION_CLEANUP_DRY_RUN=_parse_bool_env(
271+
EnvVarKeys.RETENTION_CLEANUP_DRY_RUN, default=True
272+
),
273+
RETENTION_CLEANUP_STALE_RUNNING_DAYS=int(
274+
os.environ.get(EnvVarKeys.RETENTION_CLEANUP_STALE_RUNNING_DAYS, "0")
244275
),
245276
)
246277
refreshed_environment_variables = environment_variables

agentex/src/domain/services/task_retention_service.py

Lines changed: 108 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ async def clean_task(
177177
*,
178178
enforce_idle_threshold: bool = True,
179179
idle_days: int = 7,
180+
stale_running_days: int = 0,
180181
) -> TaskCleanupResultEntity:
181182
"""
182183
Delete content-bearing rows for a stale task. Idempotent: re-running on a
@@ -189,9 +190,18 @@ async def clean_task(
189190
scheduled Temporal sweep always sets True. The admin endpoint
190191
accepts a force=true flag that flips this to False.
191192
idle_days: Idle threshold in days (when enforce_idle_threshold=True).
193+
stale_running_days: When > 0, a task whose status is RUNNING but
194+
whose last interaction is at least this many days old is treated
195+
as abandoned and may be cleaned. 0 (default) keeps the strict
196+
behavior: RUNNING tasks are never cleaned. Tasks that hang in
197+
RUNNING forever (agent crashed mid-run, workflow never reached a
198+
terminal state) would otherwise be exempt from retention
199+
indefinitely, defeating the policy for exactly the data most
200+
likely to be forgotten.
192201
193202
Refuses (raises) if:
194-
- task is currently active (status == RUNNING).
203+
- task is currently active (status == RUNNING) and not stale per
204+
stale_running_days.
195205
- enforce_idle_threshold=True and the task is not idle long enough.
196206
- unprocessed events exist past agent_task_tracker cursors.
197207
@@ -235,20 +245,31 @@ async def clean_task(
235245
events_deleted=0,
236246
)
237247

248+
# Last interaction fetched once; both the stale-idle signal and the
249+
# idle-threshold guard compare against it (no per-threshold re-query).
250+
# is_stale_idle is the "abandoned" signal that relaxes both the RUNNING
251+
# and unprocessed-events guards below.
252+
last_interaction = await self._last_interaction_at(task)
253+
is_stale_idle = stale_running_days > 0 and self._is_idle_since(
254+
last_interaction, stale_running_days
255+
)
256+
238257
# 2. Status + idle threshold guards.
239-
if task.status == TaskStatus.RUNNING:
240-
raise ClientError(
241-
f"Cannot clean task {task_id}: status is RUNNING (active)"
242-
)
243-
if enforce_idle_threshold and not await self._is_task_idle(task, idle_days):
258+
running_override = self._check_running_guard(
259+
task, is_stale_idle, emit_forensics=True
260+
)
261+
if enforce_idle_threshold and not self._is_idle_since(
262+
last_interaction, idle_days
263+
):
244264
raise ClientError(
245265
f"Cannot clean task {task_id}: not idle for {idle_days} days "
246266
f"(use force=true to override)"
247267
)
248268

249-
# 3. Unprocessed-events guard.
250-
if await self._has_unprocessed_events(task_id):
251-
raise ClientError(f"Cannot clean task {task_id}: unprocessed events remain")
269+
# 3. Unprocessed-events guard (relaxed only for the stale-RUNNING case).
270+
await self._check_unprocessed_events_guard(
271+
task_id, relax=running_override, emit_forensics=True
272+
)
252273

253274
# 4-5. Mongo deletes.
254275
messages_deleted = await self.task_message_service.delete_all_messages(task_id)
@@ -293,6 +314,7 @@ async def preview_clean_task(
293314
*,
294315
enforce_idle_threshold: bool = True,
295316
idle_days: int = 7,
317+
stale_running_days: int = 0,
296318
) -> TaskCleanupResultEntity:
297319
"""
298320
Run the same safety checks as clean_task without deleting or updating data.
@@ -306,19 +328,25 @@ async def preview_clean_task(
306328
if task.cleaned_at is not None:
307329
cleaned_at = task.cleaned_at
308330
else:
309-
if task.status == TaskStatus.RUNNING:
310-
raise ClientError(
311-
f"Cannot clean task {task_id}: status is RUNNING (active)"
312-
)
313-
if enforce_idle_threshold and not await self._is_task_idle(task, idle_days):
331+
last_interaction = await self._last_interaction_at(task)
332+
is_stale_idle = stale_running_days > 0 and self._is_idle_since(
333+
last_interaction, stale_running_days
334+
)
335+
# Preview is a non-mutating audit, so it does not emit the forensic
336+
# WARNING that a real cleanup does (keeps dry-run logs clean).
337+
running_override = self._check_running_guard(
338+
task, is_stale_idle, emit_forensics=False
339+
)
340+
if enforce_idle_threshold and not self._is_idle_since(
341+
last_interaction, idle_days
342+
):
314343
raise ClientError(
315344
f"Cannot clean task {task_id}: not idle for {idle_days} days "
316345
f"(use force=true to override)"
317346
)
318-
if await self._has_unprocessed_events(task_id):
319-
raise ClientError(
320-
f"Cannot clean task {task_id}: unprocessed events remain"
321-
)
347+
await self._check_unprocessed_events_guard(
348+
task_id, relax=running_override, emit_forensics=False
349+
)
322350
cleaned_at = datetime.now(UTC)
323351

324352
result = TaskCleanupResultEntity(
@@ -452,15 +480,66 @@ async def rehydrate_task(
452480

453481
# ---- internal helpers ----
454482

455-
async def _is_task_idle(self, task, idle_days: int) -> bool:
483+
def _check_running_guard(
484+
self, task, is_stale_idle: bool, emit_forensics: bool
485+
) -> bool:
486+
"""
487+
Raise ClientError if `task` is RUNNING, unless it is stale-idle
488+
(abandoned). A real cleanup logs the override at WARNING for forensics
489+
(cleaning a RUNNING task declares its workflow abandoned); previews pass
490+
emit_forensics=False so a dry-run audit does not produce log entries
491+
indistinguishable from a live deletion.
492+
493+
Returns True iff the stale-RUNNING override fired (task was RUNNING and
494+
abandoned). The caller uses this to decide whether to also relax the
495+
unprocessed-events guard, scoping that relaxation to exactly the
496+
stuck-RUNNING case this feature targets.
497+
"""
498+
if task.status != TaskStatus.RUNNING:
499+
return False
500+
if is_stale_idle:
501+
if emit_forensics:
502+
logger.warning(
503+
"task_cleanup_stale_running_override",
504+
extra={"task_id": task.id},
505+
)
506+
return True
507+
raise ClientError(f"Cannot clean task {task.id}: status is RUNNING (active)")
508+
509+
async def _check_unprocessed_events_guard(
510+
self, task_id: str, relax: bool, emit_forensics: bool
511+
) -> None:
512+
"""
513+
Raise ClientError if events exist past the agent_task_tracker cursor,
514+
unless `relax` (set only when the stale-RUNNING override fired). A task
515+
stuck RUNNING and abandoned will never process those events, and
516+
signal-driven agents never advance the cursor in the first place, so for
517+
that case the events are deleted with the rest. Scoped to stale-RUNNING
518+
on purpose: a terminal task with a lagging cursor keeps the strict guard
519+
so the sweep never deletes genuinely pending events. Forensics on real
520+
cleanups only (see _check_running_guard).
521+
"""
522+
if not await self._has_unprocessed_events(task_id):
523+
return
524+
if relax:
525+
if emit_forensics:
526+
logger.warning(
527+
"task_cleanup_unprocessed_events_override",
528+
extra={"task_id": task_id},
529+
)
530+
return
531+
raise ClientError(f"Cannot clean task {task_id}: unprocessed events remain")
532+
533+
async def _last_interaction_at(self, task) -> datetime | None:
456534
"""
457-
True iff the task has no interaction within the idle window.
535+
Most recent interaction timestamp for the task, or None if never.
458536
459537
Last-interaction = max(task.updated_at, latest message created_at).
460-
`task.updated_at` alone would miss tasks where the only recent
461-
activity is Mongo message writes (which don't bump the Postgres row).
538+
`task.updated_at` alone would miss tasks where the only recent activity
539+
is Mongo message writes (which don't bump the Postgres row). Issues one
540+
Mongo message-fetch; callers compute idle-ness against any threshold
541+
from the returned value rather than re-querying per threshold.
462542
"""
463-
cutoff = datetime.now(UTC) - timedelta(days=idle_days)
464543
last_interaction = task.updated_at
465544

466545
latest_messages = await self.task_message_service.get_messages(
@@ -479,9 +558,14 @@ async def _is_task_idle(self, task, idle_days: int) -> bool:
479558
if last_interaction is None or latest_at > last_interaction:
480559
last_interaction = latest_at
481560

561+
return last_interaction
562+
563+
@staticmethod
564+
def _is_idle_since(last_interaction: datetime | None, idle_days: int) -> bool:
565+
"""Pure idle check against a precomputed last-interaction timestamp."""
482566
if last_interaction is None:
483567
return True
484-
return last_interaction < cutoff
568+
return last_interaction < datetime.now(UTC) - timedelta(days=idle_days)
485569

486570
async def _has_unprocessed_events(self, task_id: str) -> bool:
487571
"""

agentex/src/domain/use_cases/task_retention_use_case.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,23 +46,30 @@ async def clean_task(
4646
task_id: str,
4747
force: bool = False,
4848
idle_days: int = 7,
49+
stale_running_days: int = 0,
4950
) -> TaskCleanupResultEntity:
5051
"""
5152
force=True is the admin escape hatch; it bypasses the idle-threshold
5253
check (but NOT the active-workflow / unprocessed-events checks, which
5354
protect correctness, not policy).
55+
56+
stale_running_days > 0 relaxes the active-workflow check for tasks that
57+
have sat in RUNNING with no interaction for at least that many days
58+
(abandoned runs that would otherwise be exempt from retention forever).
5459
"""
5560
return await self.retention_service.clean_task(
5661
task_id=task_id,
5762
enforce_idle_threshold=not force,
5863
idle_days=idle_days,
64+
stale_running_days=stale_running_days,
5965
)
6066

6167
async def preview_clean_task(
6268
self,
6369
task_id: str,
6470
force: bool = False,
6571
idle_days: int = 7,
72+
stale_running_days: int = 0,
6673
) -> TaskCleanupResultEntity:
6774
"""
6875
Dry-run counterpart to clean_task: runs the same safety checks without
@@ -72,6 +79,7 @@ async def preview_clean_task(
7279
task_id=task_id,
7380
enforce_idle_threshold=not force,
7481
idle_days=idle_days,
82+
stale_running_days=stale_running_days,
7583
)
7684

7785
async def rehydrate_task(

agentex/src/temporal/activities/retention_cleanup_activities.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ async def load_cleanup_config(self) -> dict:
7373
"page_size": env.RETENTION_CLEANUP_PAGE_SIZE,
7474
"max_in_flight": env.RETENTION_CLEANUP_MAX_IN_FLIGHT,
7575
"dry_run": env.RETENTION_CLEANUP_DRY_RUN,
76+
"stale_running_days": env.RETENTION_CLEANUP_STALE_RUNNING_DAYS,
7677
}
7778

7879
@activity.defn(name=FIND_CLEANUP_CANDIDATES_ACTIVITY)
@@ -129,7 +130,11 @@ async def find_multi_agent_cleanup_candidates(
129130

130131
@activity.defn(name=CLEAN_TASK_ACTIVITY)
131132
async def clean_task(
132-
self, task_id: str, idle_days: int, dry_run: bool = True
133+
self,
134+
task_id: str,
135+
idle_days: int,
136+
dry_run: bool = True,
137+
stale_running_days: int = 0,
133138
) -> CleanTaskOutcome:
134139
"""
135140
Delete the stored content (messages, states, events) for a single task.
@@ -139,6 +144,8 @@ async def clean_task(
139144
idle_days: Passed through to the use case for policy checks.
140145
dry_run: When omitted, preview only. Operators must pass False to
141146
enable writes.
147+
stale_running_days: When > 0, RUNNING tasks idle at least this many
148+
days are treated as abandoned and cleaned instead of skipped.
142149
143150
Returns:
144151
CleanTaskOutcome with ``status`` set to ``"cleaned"`` when content was
@@ -149,7 +156,10 @@ async def clean_task(
149156
try:
150157
if dry_run:
151158
result = await self.use_case.preview_clean_task(
152-
task_id=task_id, force=False, idle_days=idle_days
159+
task_id=task_id,
160+
force=False,
161+
idle_days=idle_days,
162+
stale_running_days=stale_running_days,
153163
)
154164
logger.info(
155165
"task_cleanup_dry_run",
@@ -164,7 +174,10 @@ async def clean_task(
164174
"events_deleted": 0,
165175
}
166176
result = await self.use_case.clean_task(
167-
task_id=task_id, force=False, idle_days=idle_days
177+
task_id=task_id,
178+
force=False,
179+
idle_days=idle_days,
180+
stale_running_days=stale_running_days,
168181
)
169182
return {
170183
"task_id": result.task_id,

0 commit comments

Comments
 (0)