fix(kanban): make claim ttl configurable
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -96,13 +96,38 @@ VALID_WORKSPACE_KINDS = {"scratch", "worktree", "dir"}
|
||||
KNOWN_TOOLSET_NAMES = frozenset(name.casefold() for name in get_toolset_names())
|
||||
_IS_WINDOWS = sys.platform == "win32"
|
||||
|
||||
# A running task's claim is valid for 15 minutes; after that the next
|
||||
# dispatcher tick reclaims it. Workers that outlive this window should call
|
||||
# ``heartbeat_claim(task_id)`` periodically. In practice most kanban
|
||||
# workloads either finish within 15m or set a longer claim explicitly.
|
||||
# A running task's claim is valid for 15 minutes by default; after that the
|
||||
# next dispatcher tick reclaims it. Workers that outlive this window should
|
||||
# call ``heartbeat_claim(task_id)`` periodically. In practice most kanban
|
||||
# workloads either finish within 15m, set a longer claim explicitly, or use
|
||||
# ``HERMES_KANBAN_CLAIM_TTL_SECONDS`` to raise the default claim window for
|
||||
# long single-call MCP workflows.
|
||||
DEFAULT_CLAIM_TTL_SECONDS = 15 * 60
|
||||
|
||||
|
||||
def _resolve_claim_ttl_seconds(ttl_seconds: Optional[int] = None) -> int:
|
||||
"""Return the effective claim TTL, honoring the kanban env override.
|
||||
|
||||
Explicit call-site values win. Otherwise a positive integer from
|
||||
``HERMES_KANBAN_CLAIM_TTL_SECONDS`` overrides the built-in default.
|
||||
Invalid or non-positive env values fall back silently so existing
|
||||
installs keep working.
|
||||
"""
|
||||
if ttl_seconds is not None:
|
||||
return max(1, int(ttl_seconds))
|
||||
|
||||
raw = os.environ.get("HERMES_KANBAN_CLAIM_TTL_SECONDS", "").strip()
|
||||
if raw:
|
||||
try:
|
||||
parsed = int(raw)
|
||||
except ValueError:
|
||||
parsed = 0
|
||||
if parsed > 0:
|
||||
return parsed
|
||||
|
||||
return DEFAULT_CLAIM_TTL_SECONDS
|
||||
|
||||
|
||||
# Worker-context caps so build_worker_context() stays bounded on
|
||||
# pathological boards (retry-heavy tasks, comment storms, giant
|
||||
# summaries). Values chosen to fit a typical 100k-char LLM prompt with
|
||||
@@ -1898,7 +1923,7 @@ def claim_task(
|
||||
conn: sqlite3.Connection,
|
||||
task_id: str,
|
||||
*,
|
||||
ttl_seconds: int = DEFAULT_CLAIM_TTL_SECONDS,
|
||||
ttl_seconds: Optional[int] = None,
|
||||
claimer: Optional[str] = None,
|
||||
) -> Optional[Task]:
|
||||
"""Atomically transition ``ready -> running``.
|
||||
@@ -1908,7 +1933,7 @@ def claim_task(
|
||||
"""
|
||||
now = int(time.time())
|
||||
lock = claimer or _claimer_id()
|
||||
expires = now + int(ttl_seconds)
|
||||
expires = now + _resolve_claim_ttl_seconds(ttl_seconds)
|
||||
with write_txn(conn):
|
||||
# Structural invariant: never transition ready -> running while any
|
||||
# parent is not yet 'done'. This is the single enforcement point
|
||||
@@ -2012,7 +2037,7 @@ def heartbeat_claim(
|
||||
conn: sqlite3.Connection,
|
||||
task_id: str,
|
||||
*,
|
||||
ttl_seconds: int = DEFAULT_CLAIM_TTL_SECONDS,
|
||||
ttl_seconds: Optional[int] = None,
|
||||
claimer: Optional[str] = None,
|
||||
) -> bool:
|
||||
"""Extend a running claim. Returns True if we still own it.
|
||||
@@ -2020,7 +2045,7 @@ def heartbeat_claim(
|
||||
Workers that know they'll exceed 15 minutes should call this every
|
||||
few minutes to keep ownership.
|
||||
"""
|
||||
expires = int(time.time()) + int(ttl_seconds)
|
||||
expires = int(time.time()) + _resolve_claim_ttl_seconds(ttl_seconds)
|
||||
lock = claimer or _claimer_id()
|
||||
with write_txn(conn):
|
||||
cur = conn.execute(
|
||||
@@ -2073,7 +2098,7 @@ def release_stale_claims(
|
||||
lock = row["claim_lock"] or ""
|
||||
host_local = lock.startswith(host_prefix)
|
||||
if host_local and row["worker_pid"] and _pid_alive(row["worker_pid"]):
|
||||
new_expires = now + int(DEFAULT_CLAIM_TTL_SECONDS)
|
||||
new_expires = now + _resolve_claim_ttl_seconds()
|
||||
with write_txn(conn):
|
||||
cur = conn.execute(
|
||||
"UPDATE tasks SET claim_expires = ? "
|
||||
@@ -3976,7 +4001,7 @@ def dispatch_once(
|
||||
conn: sqlite3.Connection,
|
||||
*,
|
||||
spawn_fn=None,
|
||||
ttl_seconds: int = DEFAULT_CLAIM_TTL_SECONDS,
|
||||
ttl_seconds: Optional[int] = None,
|
||||
dry_run: bool = False,
|
||||
max_spawn: Optional[int] = None,
|
||||
failure_limit: int = DEFAULT_SPAWN_FAILURE_LIMIT,
|
||||
|
||||
Reference in New Issue
Block a user