fix(kanban): add per-task --max-iterations override for worker budget - #19
Conversation
The dispatcher's worker-spawn env set the goal-loop turn budget and terminal timeouts but never set HERMES_MAX_ITERATIONS, so every spawned worker fell back to the global agent.max_turns default (90) regardless of task size. A large card (e.g. a multi-thousand-line reconcile) exhausts 90 iterations and times out, forcing manual recovery. The existing --goal-max-turns / goal_max_turns knob bounds the OUTER goal-loop budget and does not feed the inner agent.max_turns. Add a per-task inner-iteration budget that maps to agent.max_turns: - New nullable max_iterations INTEGER column on tasks (schema + additive migration mirroring goal_max_turns). - New --max-iterations N flag on hermes kanban create. - Surface it in hermes kanban show and --json output. - In the worker-spawn env block, set HERMES_MAX_ITERATIONS=str(value) only when the card carries one; a plain card leaves a clean env so the global default (90) is preserved. No new user-facing env var is introduced: HERMES_MAX_ITERATIONS already exists and is already honored in cli.py's budget fallback chain (config.yaml agent.max_turns > HERMES_MAX_ITERATIONS > 90); this wires the existing internal var from a per-task config column. Tests cover DB persist/default/legacy-migration, spawn-env set-when-present / clean-when-absent, a CLI flag round-trip with --json surface, and an E2E against a temp HERMES_HOME.
cwest
left a comment
There was a problem hiding this comment.
No changes needed.
The override is wired correctly. cli.py reads HERMES_MAX_ITERATIONS in its budget fallback chain (config agent.max_turns, then this env var, then 90), and the spawn block exports it only when the card carries a value, so a plain task keeps a clean env and the default holds. No new user-facing env var; the surface is the DB column and the flag.
The column, the create flag, the show line, and the --json field all mirror the existing goal_max_turns plumbing. edit is left alone, which is right: that subcommand only backfills result/summary/metadata on a done task, so a config flag there would have no home. The additive migration follows the established pattern and a legacy table without the column upgrades cleanly.
Verified by exercising the real path against a temp HERMES_HOME, not just the unit suite: create --max-iterations 250 round-trips through the DB to --json and show, and _default_spawn exports HERMES_MAX_ITERATIONS=250 for that task while a plain task (clean parent env) exports nothing. The seven new tests pass at the head SHA. Ran the kanban suite at the head SHA and at the base commit: the 27 failures are identical on both sides, all pre-existing and unrelated to this change (notify, decompose, signal-handler, stale-claim-with-live-pid), so this PR introduces none. No secrets, injection, eval, or unsafe deserialization in the added lines.
Rebased the fork stack onto upstream main@9be292f1e and dropped the per-task --max-iterations override (former P16, upstream #19). The knob cut against the board's decompose-first design: a card that exhausts the global 90-turn ceiling is almost always a sizing failure (too big, should be split into smaller cards), not a budget failure. The escape hatch for a genuinely atomic-large task already exists natively — upstream bridges HERMES_MAX_ITERATIONS from agent.max_turns in config — so the per-task DB column was redundant carry with no upstream home. Stack is now 17 patches.
Rebased the fork stack onto upstream main@9be292f1e and dropped the per-task --max-iterations override (former P16, upstream #19). The knob cut against the board's decompose-first design: a card that exhausts the global 90-turn ceiling is almost always a sizing failure (too big, should be split into smaller cards), not a budget failure. The escape hatch for a genuinely atomic-large task already exists natively — upstream bridges HERMES_MAX_ITERATIONS from agent.max_turns in config — so the per-task DB column was redundant carry with no upstream home. Stack is now 17 patches. (cherry picked from commit 94d1076)
What
Adds a per-task inner-iteration budget override so a large kanban card can raise the worker's
agent.max_turnsceiling instead of timing out at the global default (90).Why
The dispatcher's worker-spawn env block set the goal-loop turn budget and terminal timeouts but never set
HERMES_MAX_ITERATIONS, so every spawned worker fell back to the globalagent.max_turnsdefault (90) regardless of task size. A large card exhausts 90 iterations and times out, forcing manual recovery. (This very fix's first implementation run timed out at the 90-iteration ceiling — the bug fixing itself.)The existing
--goal-max-turns/goal_max_turnsknob bounds the outer goal-loop budget and does not feed the inneragent.max_turns.How
max_iterations INTEGERcolumn ontasks(schema + additive migration mirroringgoal_max_turns).--max-iterations Nflag onhermes kanban create.hermes kanban showand--json.HERMES_MAX_ITERATIONS=str(value)only when the card carries one; a plain card leaves a clean env so the global default (90) is preserved.No new env var
HERMES_MAX_ITERATIONSalready exists and is already honored incli.py's budget fallback chain (config.yaml agent.max_turns>HERMES_MAX_ITERATIONS> 90). This wires the existing internal var from a per-task config column — the user-facing surface is the DB column + CLI flag, not an env var.Tests
--jsonsurfaceHERMES_HOMEexercising the realhermes kanban create/showpath241 kanban tests pass; ruff clean. Carried in PATCHES.md as
upstream-pending.