Skip to content

fix(kanban): hold reclaim while the worker is still alive - #49064

Merged
teknium1 merged 3 commits into
mainfrom
salvage/44909-kanban-reclaim-hold
Jun 19, 2026
Merged

fix(kanban): hold reclaim while the worker is still alive#49064
teknium1 merged 3 commits into
mainfrom
salvage/44909-kanban-reclaim-hold

Conversation

@teknium1

Copy link
Copy Markdown
Contributor

Summary

The kanban dispatcher no longer spawns a duplicate worker beside one that's still alive — fixing the self-reinforcing runaway observed at 100+ workers on a single board.

Salvage of #44909 by @Sahil-SS9 onto current main (authorship preserved via cherry-pick).

Root cause

release_stale_claims / detect_stale_running reclaimed a TTL-expired task unconditionally, even when the kill we issued didn't land. A worker parked in uninterruptible D-state by a cgroup memory.high throttle can't receive SIGKILL until the throttle lifts — so the claim got released and the dispatcher spawned a second worker beside the still-alive first. More workers → more memory pressure → more throttling → more duplicates.

Changes

  • hermes_cli/kanban_db.py:
    • _worker_survived_termination(termination) — true only when we attempted to kill our own host-local worker and it's still alive (attempted ∧ host_local ∧ ¬terminated). Foreign-host claims and no-op attempts fall through to normal release.
    • _defer_reclaim_for_live_worker(...) — extends claim_expires by RECLAIM_DEFER_GRACE_SECONDS (120s), keeps the task running (no duplicate spawn), and records a reclaim_deferred event visible in hermes kanban tail.
    • Both release_stale_claims and detect_stale_running now defer instead of release when the worker survived termination. Self-correcting: not spawning a duplicate is what finally lets the throttled worker die, after which the next tick reclaims cleanly.

Validation

  • scripts/run_tests.sh tests/hermes_cli/test_kanban_db.py → 218/218 pass, including 4 new cases: defer-on-survival, reclaim-on-kill-success, release-when-not-host-local, detect_stale defers.
  • py_compile clean.

Closes #44909.

Infographic

hold-reclaim-while-worker-lives

@github-actions

github-actions Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

🔎 Lint report: salvage/44909-kanban-reclaim-hold vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 11069 on HEAD, 11067 on base (🆕 +2)

🆕 New issues (1):

Rule Count
invalid-assignment 1
First entries
tests/run_agent/test_credits_notices_toggle.py:76: [invalid-assignment] invalid-assignment: Object of type `None` is not assignable to attribute `_credits_session_start_micros` of type `int`

✅ Fixed issues (2):

Rule Count
unresolved-attribute 2
First entries
run_agent.py:2971: [unresolved-attribute] unresolved-attribute: Object of type `Self@get_credits_spent_micros` has no attribute `_credits_session_start_micros`
tests/run_agent/test_credits_notices_toggle.py:76: [unresolved-attribute] unresolved-attribute: Unresolved attribute `_credits_session_start_micros` on type `AIAgent`

Unchanged: 5793 pre-existing issues carried over.

Diagnostics are surfaced as warnings — this check never fails the build.

@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/cron Cron scheduler and job management labels Jun 19, 2026
Sahil-SS9 and others added 3 commits June 19, 2026 07:25
release_stale_claims and detect_stale_running call _terminate_reclaimed_worker
and then release the task claim unconditionally, even when the termination did
not actually kill the worker. _terminate_reclaimed_worker already reports this
via its "terminated" flag, but the callers ignore it.

When a worker is parked in uninterruptible (D) state — for example throttled by
a cgroup memory.high limit — a pending SIGTERM/SIGKILL cannot be delivered until
the throttle lifts, so the kill is a no-op. The dispatcher then frees the claim
and spawns a fresh worker beside the still-alive one. Repeated every dispatch
tick this accumulates duplicate workers without bound, deepening the memory
pressure that caused the throttle in the first place — a self-reinforcing
runaway.

Fix: gate both automatic reclaim paths on _worker_survived_termination(). When
we attempted to kill our own host-local worker and it is still alive, defer the
reclaim (_defer_reclaim_for_live_worker extends the claim a short grace and
emits a reclaim_deferred event) instead of releasing. This guarantees at most
one live worker per task and is self-correcting: not spawning a duplicate is
what relieves the pressure so the pending signal lands and the worker dies, and
the next tick reclaims cleanly. Non-host-local claims and the operator-driven
reclaim_task() path keep their existing force-release behaviour.

Related: #41448 (concurrent dispatchers amplify this by doubling reclaim
frequency); #42858 (kill the worker rather than orphan it on archive).

Tests: defer-when-worker-survives, reclaim-when-killed,
release-when-not-host-local, and the detect_stale_running path.
_terminate_reclaimed_worker early-returned on ProcessLookupError with
terminated=False. The new reclaim-defer guard reads that as 'worker
survived the kill' and defers the reclaim forever, so a stale task whose
worker is already dead never lands in result.stale. ProcessLookupError
means the process is gone — that IS a successful termination. Split it
from the generic OSError branch and set terminated=True.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cron Cron scheduler and job management P2 Medium — degraded but workaround exists type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants