fix(kanban): bind per-task affinity scope around the goal-loop judge call
run_kanban_goal_loop judges every worker turn outside any agent turn, so on a relay target each judge call 400'd MissingSessionID and the loop burned its turn budget on transport-failed 'continue' verdicts. Bind kanban:<task_id> around the call (same shape as the two handoff gates and kanban_specify), keeping an already-bound scope. Adds one invariant test on the loop.
This commit is contained in:
@@ -1646,7 +1646,15 @@ def run_kanban_goal_loop(
|
||||
_log(f"kanban goal loop: task {task_id} status={status!r}; stopping")
|
||||
return _result("stopped", f"status={status}")
|
||||
|
||||
verdict, reason, _parse_failed, _wait, _transport_failed = judge_goal(goal_text, last_response)
|
||||
# The between-turns judge runs outside any agent turn: bind the per-task relay-affinity
|
||||
# scope (same shape as the handoff gates) so the relay does not reject the call (#113669).
|
||||
from agent.portal_tags import get_affinity_scope, reset_affinity_scope, set_affinity_scope
|
||||
affinity_token = None if get_affinity_scope() else set_affinity_scope(f"kanban:{task_id}")
|
||||
try:
|
||||
verdict, reason, _parse_failed, _wait, _transport_failed = judge_goal(goal_text, last_response)
|
||||
finally:
|
||||
if affinity_token is not None:
|
||||
reset_affinity_scope(affinity_token)
|
||||
if verdict == "wait":
|
||||
verdict = "continue"
|
||||
_log(f"kanban goal loop: turn {turns_used}/{max_turns} verdict={verdict} reason={_truncate(reason, 120)}")
|
||||
|
||||
@@ -85,3 +85,28 @@ def test_tool_gate_binds_per_task_affinity_scope():
|
||||
kanban_tools._goal_gate("kanban_complete", _task("task-9"), "task-9", "ev")
|
||||
assert seen == ["kanban:task-9"]
|
||||
assert get_affinity_scope() is None
|
||||
|
||||
|
||||
def test_goal_loop_judge_binds_per_task_affinity_scope():
|
||||
"""The between-turns judge in run_kanban_goal_loop runs under kanban:<task_id> too."""
|
||||
from hermes_cli import goals
|
||||
|
||||
seen = []
|
||||
|
||||
def fake_judge(goal, last_response):
|
||||
seen.append(get_affinity_scope())
|
||||
return ("continue", "not yet", False, None, False)
|
||||
|
||||
with patch.object(goals, "judge_goal", side_effect=fake_judge):
|
||||
result = goals.run_kanban_goal_loop(
|
||||
task_id="task-7",
|
||||
goal_text="goal",
|
||||
run_turn=lambda prompt: "still working",
|
||||
task_status_fn=lambda: "running",
|
||||
block_fn=lambda msg: None,
|
||||
max_turns=2,
|
||||
first_response="first",
|
||||
)
|
||||
assert result["outcome"] == "blocked_budget"
|
||||
assert seen == ["kanban:task-7", "kanban:task-7"]
|
||||
assert get_affinity_scope() is None
|
||||
|
||||
Reference in New Issue
Block a user