diff --git a/gateway/session_kanban.py b/gateway/session_kanban.py index d59ed2eecf..19ec95340d 100644 --- a/gateway/session_kanban.py +++ b/gateway/session_kanban.py @@ -348,19 +348,26 @@ def worker_exit_code(path, params): return worker_result(path, params).get('exit_code', 1) +# Who wrote a card attempt's prompts: the dispatcher, never the operator, so memory providers do not +# file "work kanban task t_..." and the worker's progress notes as the human's own turns (#137406). +# Not a ``bot:`` id: a Bot Mode profile named ``kanban`` would share its memory key. +KANBAN_TURN_AUTHOR = {'id': 'kanban:dispatcher', 'name': 'Kanban dispatcher', 'is_bot': True} + + def _run_task_turns(agent, frame, history, context): from agent.skill_commands import build_preloaded_skills_prompt skills, loaded, missing = build_preloaded_skills_prompt(context['skills'], task_id=agent.session_id) if missing and not loaded: raise ValueError('kanban_skills_unavailable') prompt = '\n\n'.join(p for p in (skills, context['context'], frame['text']) if p) - result = agent.run_conversation(prompt, conversation_history=history) + result = agent.run_conversation(prompt, conversation_history=history, turn_author=KANBAN_TURN_AUTHOR) def recover(nudge): nonlocal result prior = result.get('messages') if isinstance(result, dict) else None result = agent.run_conversation(nudge, conversation_history=prior or - agent._session_db.get_messages_as_conversation(agent.session_id)) + agent._session_db.get_messages_as_conversation(agent.session_id), + turn_author=KANBAN_TURN_AUTHOR) # Same in-place retry as the one-shot CLI worker (agent/kanban_turn_recovery.py), but the # carrier is this frame's bound claim, never the owner process's environment. from agent.kanban_turn_recovery import recover_failed_kanban_turns @@ -382,7 +389,8 @@ def _run_task_turns(agent, frame, history, context): kb.block_task(conn, context['task_id'], reason=reason, expected_run_id=context['run_id']) def turn(prompt): nonlocal result - result = agent.run_conversation(prompt, conversation_history=agent._session_db.get_messages_as_conversation(agent.session_id)) + result = agent.run_conversation(prompt, conversation_history=agent._session_db.get_messages_as_conversation(agent.session_id), + turn_author=KANBAN_TURN_AUTHOR) return result.get('final_response', '') run_kanban_goal_loop(task_id=context['task_id'], goal_text=context['goal_text'], run_turn=turn, task_status_fn=status, block_fn=block, max_turns=context['goal_max_turns'] or DEFAULT_MAX_TURNS, diff --git a/tests/agent/test_kanban_turn_recovery.py b/tests/agent/test_kanban_turn_recovery.py index 87cc003321..c38b8e2edc 100644 --- a/tests/agent/test_kanban_turn_recovery.py +++ b/tests/agent/test_kanban_turn_recovery.py @@ -493,7 +493,7 @@ def owner_worker(tmp_path, clear_kanban_env, monkeypatch): def get_messages_as_conversation(self, _sid): return [{"role": "user", "content": "from-store"}] - def run_conversation(self, message, conversation_history=None): + def run_conversation(self, message, conversation_history=None, **_kwargs): entered.append(message) result = script() if callable(script) else script[seen["n"]] seen["n"] += 1 diff --git a/tests/gateway/test_session_kanban_turn_recovery.py b/tests/gateway/test_session_kanban_turn_recovery.py index 43a81de2f2..b3bba4c28b 100644 --- a/tests/gateway/test_session_kanban_turn_recovery.py +++ b/tests/gateway/test_session_kanban_turn_recovery.py @@ -15,14 +15,15 @@ class _Agent: session_id = "s1" def __init__(self, results): - self.results, self.calls = list(results), [] + self.results, self.calls, self.authors = list(results), [], [] self._session_db = self def get_messages_as_conversation(self, _sid): return [{"role": "user", "content": "from-store"}] - def run_conversation(self, message, conversation_history=None): + def run_conversation(self, message, conversation_history=None, turn_author=None): self.calls.append((message, conversation_history)) + self.authors.append(turn_author) return self.results.pop(0) @@ -63,6 +64,8 @@ def test_failed_turn_retries_in_place_from_frame_carrier(tmp_path, monkeypatch): assert context["task_id"] in nudge and "t_foreign" not in nudge assert history == FAILED["messages"] # same session context, not a cold restart assert session_kanban.worker_exit_code(path, context) == 0 + # Both the first turn and the in-place retry are the dispatcher's, never the operator's (#137406). + assert agent.authors == [{"id": "kanban:dispatcher", "name": "Kanban dispatcher", "is_bot": True}] * 2 def test_unprovable_claim_denies_retry_and_goal_continuation(tmp_path, monkeypatch):