From 3fbbf588531fabe2dc3d006142de4f8f4f26bd4b Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Sun, 10 May 2026 09:12:10 -0700 Subject: [PATCH] docs(kanban): document max_spawn as live concurrency cap (not per-tick budget) Follow-up to the previous commit's behavior fix. Adds a paragraph to dispatch_once's docstring making the concurrency-cap semantic explicit, and an inline comment near the running_count query explaining why we do the count (so a future reader doesn't refactor it back to per-tick semantics thinking it's redundant). Both call out the unbounded-accumulation failure mode that motivated the fix, since nothing in the codebase or skills currently documents what max_spawn is supposed to mean. The semantic is per-board: each kanban board has its own SQLite file, so the running-count COUNT(*) is naturally scoped to the board the dispatcher tick is processing. --- hermes_cli/kanban_db.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/hermes_cli/kanban_db.py b/hermes_cli/kanban_db.py index 967db790efa..5ee6f28385e 100644 --- a/hermes_cli/kanban_db.py +++ b/hermes_cli/kanban_db.py @@ -3609,6 +3609,14 @@ def dispatch_once( failures the task is auto-blocked with the last error as its reason — prevents the dispatcher from thrashing forever on an unfixable task. + ``max_spawn`` is a **live concurrency cap**, not a per-tick spawn budget: + it counts tasks already in ``status='running'`` plus this tick's spawns + against the limit. So ``max_spawn=4`` means "at most 4 workers running + at any time across the whole board" — matching the gateway's stated + intent ("limit concurrent kanban tasks"). With a per-tick interpretation + a 60-second tick interval could grow concurrency by N every minute on a + busy board and accumulate without bound. + ``spawn_fn`` defaults to ``_default_spawn``. Tests pass a stub. ``board`` pins workspace/log/db resolution for this tick to a specific board. When omitted, the current-board resolution chain is used. @@ -3660,6 +3668,13 @@ def dispatch_once( result.timed_out = enforce_max_runtime(conn) result.promoted = recompute_ready(conn) + # Count tasks already running so max_spawn enforces concurrency rather + # than a per-tick spawn budget. See the docstring above for the full + # rationale; the short version is that a 60-second tick interval with a + # per-tick budget of N would grow concurrency by N every tick on a busy + # board, since "running" tasks aren't reclaimed by completion alone — + # they sit in status='running' until the worker calls + # kanban_complete/kanban_block (or the dispatcher TTL-reclaims them). running_count = 0 if max_spawn is not None: running_count = int(