diff --git a/conductor/tracks.md b/conductor/tracks.md index 513e5a4e..ebae93d9 100644 --- a/conductor/tracks.md +++ b/conductor/tracks.md @@ -283,5 +283,5 @@ This file tracks all major tracks for the project. Each track has its own detail --- -- [ ] **Track: Fix MiniMax history sequencing and truncation** +- [x] **Track: Fix MiniMax history sequencing and truncation** *Link: [./tracks/minimax_history_fix_20260601/](./tracks/minimax_history_fix_20260601/)* diff --git a/conductor/tracks/minimax_history_fix_20260601/plan.md b/conductor/tracks/minimax_history_fix_20260601/plan.md index 52a982bd..d0cf4f79 100644 --- a/conductor/tracks/minimax_history_fix_20260601/plan.md +++ b/conductor/tracks/minimax_history_fix_20260601/plan.md @@ -1,18 +1,18 @@ # Implementation Plan: MiniMax History Fix ## Phase 1: Implementation -- [ ] Task: Implement History Repair - - [ ] Create `_repair_minimax_history(history: list[dict[str, Any]])` in `src/ai_client.py`. - - [ ] Logic: Check if the last message is `assistant` with `tool_calls`. If so, append a `tool` message for each `tool_call_id` with an error message indicating the session was interrupted. -- [ ] Task: Implement History Truncation - - [ ] Create `_trim_minimax_history(system_blocks: list[dict[str, Any]], history: list[dict[str, Any]])` in `src/ai_client.py` (adapt from `_trim_anthropic_history`). - - [ ] Logic: Iteratively remove oldest assistant/user pairs if the estimated tokens exceed the context limit. -- [ ] Task: Integrate into `_send_minimax` - - [ ] Call `_repair_minimax_history(_minimax_history)` within the initial `_minimax_history_lock` block. - - [ ] Call `_trim_minimax_history` at the start of the `for round_idx` loop. - - [ ] Log dropped messages to `_append_comms`. +- [x] Task: Implement History Repair + - [x] Create `_repair_minimax_history(history: list[dict[str, Any]])` in `src/ai_client.py`. + - [x] Logic: Check if the last message is `assistant` with `tool_calls`. If so, append a `tool` message for each `tool_call_id` with an error message indicating the session was interrupted. +- [x] Task: Implement History Truncation + - [x] Create `_trim_minimax_history(system_blocks: list[dict[str, Any]], history: list[dict[str, Any]])` in `src/ai_client.py` (adapt from `_trim_anthropic_history`). + - [x] Logic: Iteratively remove oldest assistant/user pairs if the estimated tokens exceed the context limit. +- [x] Task: Integrate into `_send_minimax` + - [x] Call `_repair_minimax_history(_minimax_history)` within the initial `_minimax_history_lock` block. + - [x] Call `_trim_minimax_history` at the start of the `for round_idx` loop. + - [x] Log dropped messages to `_append_comms`. ## Phase 2: Verification -- [ ] Task: Verification - - [ ] Verify that switching to MiniMax and executing a tool call, then forcibly stopping (e.g., clearing the app or throwing an error in a tool), allows the next request to succeed. -- [ ] Task: Conductor - User Manual Verification 'Phase 2: Verification' (Protocol in workflow.md) \ No newline at end of file +- [x] Task: Verification + - [x] Verify that switching to MiniMax and executing a tool call, then forcibly stopping (e.g., clearing the app or throwing an error in a tool), allows the next request to succeed. +- [x] Task: Conductor - User Manual Verification 'Phase 2: Verification' (Protocol in workflow.md) \ No newline at end of file diff --git a/src/ai_client.py b/src/ai_client.py index b7c188bc..6eb482b6 100644 --- a/src/ai_client.py +++ b/src/ai_client.py @@ -2091,6 +2091,48 @@ def _list_minimax_models(api_key: str) -> list[str]: pass return ["MiniMax-M2.7", "MiniMax-M2.5", "MiniMax-M2.1", "MiniMax-M2"] +def _repair_minimax_history(history: list[dict[str, Any]]) -> None: + if not history: + return + last = history[-1] + if last.get("role") != "assistant": + return + tool_calls = last.get("tool_calls", []) + if not tool_calls: + return + call_ids = [] + for tc in tool_calls: + if hasattr(tc, "id"): call_ids.append(tc.id) + elif isinstance(tc, dict) and tc.get("id"): call_ids.append(tc["id"]) + + for cid in call_ids: + already_has = any(m.get("role") == "tool" and m.get("tool_call_id") == cid for m in history[-len(call_ids)-1:]) + if not already_has: + history.append({ + "role": "tool", + "tool_call_id": cid, + "content": "ERROR: Session was interrupted before tool result was recorded.", + }) + +def _trim_minimax_history(system_blocks: list[dict[str, Any]], history: list[dict[str, Any]]) -> int: + est = _estimate_prompt_tokens(system_blocks, history) + limit = 180_000 + if est <= limit: + return 0 + dropped = 0 + while len(history) > 3 and est > limit: + if history[1].get("role") == "assistant" and len(history) > 2 and history[2].get("role") == "user": + removed_asst = history.pop(1) + removed_user = history.pop(1) + dropped += 2 + est -= _estimate_message_tokens(removed_asst) + est -= _estimate_message_tokens(removed_user) + else: + removed = history.pop(1) + dropped += 1 + est -= _estimate_message_tokens(removed) + return dropped + def _ensure_minimax_client() -> None: global _minimax_client if _minimax_client is None: @@ -2121,6 +2163,7 @@ def _send_minimax(md_content: str, user_message: str, base_dir: str, client = OpenAI(api_key=api_key, base_url="https://api.minimax.io/v1") with _minimax_history_lock: + _repair_minimax_history(_minimax_history) if discussion_history and not _minimax_history: user_content = f"[DISCUSSION HISTORY]\n\n{discussion_history}\n\n---\n\n{user_message}" else: @@ -2137,6 +2180,10 @@ def _send_minimax(md_content: str, user_message: str, base_dir: str, current_api_messages.append(sys_msg) with _minimax_history_lock: + dropped = _trim_minimax_history([sys_msg], _minimax_history) + if dropped > 0: + _append_comms("OUT", "request", {"message": f"[MINIMAX HISTORY TRIMMED: dropped {dropped} old messages]"}) + for i, msg in enumerate(_minimax_history): role = msg.get("role") api_msg = {"role": role}