fix(kanban): hold external/interactive claims so the dispatcher stops re-dispatching live operator sessions - #51386
Open
vk0dev wants to merge 1 commit into
Open
Conversation
…patch
A live interactive Claude Code session (tmux ccz/ccp) that runs
`hermes kanban claim <id>` does not emit kanban heartbeats or Hermes API
traffic, so its claim_expires/last_heartbeat_at go stale exactly like a
wedged worker. release_stale_claims() could not tell the two apart and
reclaimed the task, after which the dispatcher spawned a parallel coder
worker on it — the t_0781f106 parallel-work incident (double work, merge
conflicts, wt/* worktree sprawl).
Introduce an explicit external/interactive claim hold:
- New nullable `tasks.claim_kind` column ('worker' | 'external'; NULL =
legacy/worker). Additive migration; existing boards keep current
behaviour.
- claim_task(external=True) marks the claim 'external' and holds it
indefinitely (claim_expires = NULL). Such a row is invisible to all
three reclaimers: release_stale_claims (filters claim_expires IS NOT
NULL), enforce_max_runtime and detect_crashed_workers (filter
worker_pid IS NOT NULL — an external hold has none). A claim_kind guard
in release_stale_claims adds defense-in-depth.
- CLI: `hermes kanban claim <id> --external` (opt-in; default claim path
unchanged, stays a worker claim with a TTL). Released only by explicit
complete / block / reclaim.
- complete/block/reclaim clear claim_kind so a reclaimed external task is
cleanly worker-claimable again (operator recovery path for a dead
interactive session).
Real reclaim of dead/wedged WORKER claims is unchanged (regression-
guarded). Tests cover: external claim survives stale sweep (indefinite +
forced-expiry guard), worker claim still reclaimed, CLI --external
contract, complete/block/reclaim clear the hold, and the additive
migration.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
tonydwb
approved these changes
Jun 23, 2026
tonydwb
left a comment
There was a problem hiding this comment.
Code Review Summary
Verdict: Approved
Well-designed fix for the kanban dispatcher re-dispatching live operator sessions. External (interactive) claims are now held indefinitely and never auto-reclaimed.
Looks Good
- New
claim_kindcolumn (workervsexternal) cleanly discriminates claim types - External claims set
claim_expires = NULLandworker_pid = NULL, making them invisible to all three reclaimers - DB migration is additive (new column, backward-compatible with legacy rows)
- CLI
--externalflag with clear help text explaining the behavior - Defense-in-depth:
release_stale_claimsadditionally guards onclaim_kind - Well-documented with extensive comments explaining the design rationale
Reviewed by Hermes Agent
Contributor
|
Thanks for isolating the interactive-operator claim case. The underlying issue remains reproducible on current main: Problems
Suggested changes
This is an automated hermes-sweeper review. |
This was referenced Jul 28, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
A live interactive operator session (e.g. a Claude Code session in tmux) that runs
hermes kanban claim <id>does not emit kanban heartbeats or other Hermes API traffic. Itsclaim_expires/last_heartbeat_attherefore go stale exactly like a wedged worker, andrelease_stale_claims()cannot tell the two apart. It reclaims the task, and the next dispatcher tick spawns a parallel worker on a task a human is actively working on — double work, merge conflicts, andwt/*worktree sprawl.This is distinct from the cgroup-throttle duplication that
RECLAIM_DEFER_GRACE_SECONDSaddresses: here the claim holder is a healthy interactive process that legitimately never heartbeats.Fix — explicit external/interactive claim hold
tasks.claim_kindcolumn ('worker'|'external';NULL= legacy/worker). Additive migration via_migrate_add_optional_columns; existing boards keep current behaviour.claim_task(external=True)marks the claim'external'and holds it indefinitely (claim_expires = NULL). Such a row is invisible to all three reclaimers:release_stale_claims(filtersclaim_expires IS NOT NULL),enforce_max_runtimeanddetect_crashed_workers(filterworker_pid IS NOT NULL— an external hold has none). Aclaim_kindguard inrelease_stale_claimsadds defense-in-depth.hermes kanban claim <id> --external(opt-in; the default claim path is unchanged — still a worker claim with a TTL). An external hold is released only by an explicitcomplete/block/reclaim.complete/block/reclaimclearclaim_kind, so a reclaimed external task is cleanly worker-claimable again (operator recovery path for a dead interactive session).Reclaim of genuinely dead/wedged worker claims is unchanged (regression-guarded).
Tests
Adds coverage in
test_kanban_db.pyandtest_kanban_cli_dispatch_passthrough.py: external claim survives the stale sweep (indefinite + forced-expiry guard), worker claim still reclaimed,--externalCLI contract,complete/block/reclaimclear the hold, and the additive migration. Full affected-file suites pass (239 tests).🤖 Generated with Claude Code