23 KiB
Track: AI Loop Regressions (MiniMax, Gemini, Gemini CLI, DeepSeek)
Status: Active (spec approved 2026-06-14) Initialized: 2026-06-14 Owner: Tier 2 Tech Lead Priority: High (4 providers broken in production; user-facing symptom)
1. Overview
This track diagnoses and fixes 4 user-visible regressions in the AI loop that surfaced after the data_oriented_error_handling_20260606 track shipped (2026-06-12) and the subsequent ai client pass commit 5030bd84 (2026-06-13, 503-line src/ai_client.py refactor in the Gemini region). The regressions affect MiniMax (M2.x), Gemini, Gemini CLI, and DeepSeek — the 4 providers most heavily touched by the refactor.
The reported symptoms (per user 2026-06-14):
- Thinking monologues no longer render in the Discussion Hub.
- AI turns do not get entries in the Discussion Hub; the user must manually add them via the
Historybutton.
The 2 symptoms are the visible result of 3 distinct bugs interacting. Bug #2 is the primary culprit for the "no entries" symptom; Bug #3 is the primary culprit for the "no thinking" symptom on MiniMax; Bug #1 is dead code that breaks the error-reporting path. The user-supplied screenshots show entries in the Operations Hub Comms History and in the Comms History panel — confirming the requests reach the AI client and responses are emitted, but the response doesn't propagate to the discussion panel.
2. Goals (Priority Order)
| Priority | Goal | Rationale |
|---|---|---|
| A (primary value) | Fix Bug #2: _handle_request_event (the live AI send path) routes send_result() errors back into the Discussion Hub as error entries, restoring the pre-refactor UX. |
The "no entries" symptom is the user-blocking bug. Fixing it makes the AI loop immediately usable again. |
| A (primary value) | Fix Bug #3: MiniMax thinking content (reasoning_details[0].text) is wrapped in <thinking>...</thinking> tags in the returned text, so thinking_parser.parse_thinking_trace can extract it and the discussion entry shows the thinking segment. |
MiniMax is the user's current provider; thinking monologues are a core feature. Without this fix the user cannot see the AI's reasoning. |
| B (architectural) | Fix Bug #1: replace the 3 dead except ai_client.ProviderError as e: clauses in src/app_controller.py with the equivalent send_result() + if not result.ok: ... pattern. |
The dead clauses silently swallow the AttributeError that arises when Python tries to evaluate ai_client.ProviderError to compare against the in-flight exception. The replacement aligns with the data-oriented error handling convention and gives Tier 2 a clean reference for the planned public_api_migration_20260606 follow-up. |
| C (diagnostic) | Root-cause verification: each of the 3 fixes is preceded by a failing TDD test that reproduces the bug, and a commit history audit is documented in the spec. | The user explicitly asked for an investigation track. The diagnostic tests are the empirical evidence for each root cause. |
| D (forward-looking) | Document the deferred Gemini / Gemini CLI thinking-format investigation as a follow-up note in docs/guide_ai_client.md "See Also" section. |
The user's complaint includes Gemini, but the format-compatibility issue is plausibly a pre-existing limitation, not a new regression. Documented as a follow-up to avoid scope creep. |
2.1 Non-Goals (this track)
- Not migrating the 5 remaining production call sites or 63 test call sites to
send_result(). The plannedpublic_api_migration_20260606follow-up track handles that. This track only migrates the 3 sites that are actively broken (the deadexceptclauses inapp_controller.py:305, 313, 3692) — the minimum needed to make the live path work. - Not expanding the
thinking_parser.pycontract to support new marker formats. The<thinking>,<thought>, andThinking:markers are the canonical set; the MiniMax fix uses the existing<thinking>format (matches DeepSeek's pattern). - Not investigating or fixing the Gemini / Gemini CLI thinking-format compatibility (deferred; see §13.1).
- Not changing the
ProviderErrorremoval (it was correctly removed in commit64b787b8); we only fix the dead except clauses. - Not adding a new
thinking_parserformat; we work within the existing 3-marker contract.
3. Current State Audit (as of commit 5030bd84)
3.1 Already Implemented (DO NOT re-implement)
src/result_types.py:Result[T],ErrorInfo,ErrorKinddataclasses exist;Result.data: T+Result.errors: list[ErrorInfo]is the canonical pattern.src/ai_client.py:send_result()(lines 2970-3092): the new public entry point, returnsResult[str]. Routes to_send_<vendor>_result()per provider.src/ai_client.py:send()(lines 2907-2968): the@deprecatedshim, callssend_result()and returnsresult.data. Never raises on error — returns""instead.src/ai_client.py:_send_*_result()(lines 1291-3082): all 9 vendors (anthropic,gemini,gemini_cli,deepseek,minimax,qwen,grok,llama,llama_native) returnResult[str]withErrorInfoon failure.src/ai_client.py:run_with_tool_loop()(lines 734-836): already extracts reasoning viareasoning_extractorand stores it inhistory[].reasoning_content. The reasoning content is in the history but NOT in the returned text.src/thinking_parser.py:parse_thinking_trace()(lines 8-54): already extracts<thinking>,<thought>, andThinking:prefix segments.src/app_controller.py:_on_comms_entry()(lines 3749-3840): already routesresponsecomms entries to_pending_history_addsiftext_content.strip()is truthy andparse_thinking_tracefinds segments.- DeepSeek's reasoning wrap pattern (
src/ai_client.py:2117-2118): DeepSeek wrapsreasoning_contentin<thinking>...</thinking>tags in the final text before returning. This is the reference pattern for the MiniMax fix.
3.2 Gaps to Fill (This Track's Scope)
| # | File:line | Gap | Symptom |
|---|---|---|---|
| G1 | src/app_controller.py:3677-3697 |
_handle_request_event calls deprecated ai_client.send() and discards the result. On error, result.data == "" is queued as a response comms entry, but _on_comms_entry at line 3801 filters it out via if text_content.strip():. No discussion entry is added. |
"AI turns are not getting proper entries" |
| G2 | src/app_controller.py:305, 313, 3692 |
Three except ai_client.ProviderError as e: clauses reference a class that was removed in commit 64b787b8. Python evaluates the class on every raised exception; on missing class, the except clause itself raises AttributeError. The error path is broken. |
Silently dropped error messages (compounding G1) |
| G3 | src/ai_client.py:797-836, 2418-2443 |
_send_minimax() uses reasoning_extractor to extract reasoning into history[].reasoning_content, but the returned response_text (and thus Result.data) does not include the thinking tags. parse_thinking_trace finds no <thinking> blocks, so no thinking segments are added to the discussion entry. |
"Thinking monologues no longer rendering" (MiniMax) |
| G4 | (deferred) src/ai_client.py:_send_gemini, _send_gemini_cli |
Gemini SDK output may include thinking in a format that parse_thinking_trace doesn't match. Empirical verification needed. |
"Thinking monologues no longer rendering" (Gemini) |
4. Functional Requirements
FR1: Error response becomes a discussion entry (Bug #2 / G1)
_handle_request_event in src/app_controller.py:3677-3697 must:
- Call
ai_client.send_result()instead ofai_client.send(). - On
result.ok == False: queue aresponsecomms entry withtext=ui_error_message(),status="error",role="Vendor API"so the user sees the error in both the AI response panel AND as a discussion entry. - On
result.ok == True: queue aresponsecomms entry withtext=result.data,status="done",role="AI"(preserves current behavior). - Update
_ai_statustof"error: {ui_error_message()}"on failure (preserves the visible status indicator). - Preserve the existing streaming path (
_on_ai_streamcontinues to receive chunks duringstream=Trueexecution).
FR2: Replace dead except ai_client.ProviderError clauses (Bug #1 / G2)
All 3 sites in src/app_controller.py (305, 313, 3692) must:
- Remove the
except ai_client.ProviderErrorclause. - Replace with either:
- For sites that call
ai_client.send(): callai_client.send_result()instead; ifnot result.ok, route the error to the API response (HTTPException for the API sites, comms queue for the live site). - For sites that call other
ai_clientmethods that raise: use a genericexcept Exceptionand convert to a structured response (HTTPException for API sites, error entry for the live site).
- For sites that call
- Reference the data-oriented error handling styleguide (
conductor/code_styleguides/error_handling.md§3.1) in the resulting code's docstring (so future migrations follow the same pattern).
FR3: MiniMax thinking content reaches parse_thinking_trace (Bug #3 / G3)
_send_minimax in src/ai_client.py:2418-2443 (or run_with_tool_loop at lines 797-836) must:
-
When
caps.reasoningis True AND the previous round extracted non-emptyreasoning_content, the NEXT round'sresponse_text(andResult.data) must include the reasoning wrapped in<thinking>...</thinking>tags (matching DeepSeek's pattern atsrc/ai_client.py:2117-2118). -
The
run_with_tool_loophistory write at line 808 must continue to store the rawreasoning_content(so subsequent API calls can use it for the next turn's reasoning). The thinking tag wrapping is additive: the raw reasoning is in the history, the tagged reasoning is in the visible text. -
The
<think>...</think>format used by some MiniMax models (visible in the user-supplied screenshot 1) must continue to work —parse_thinking_tracealready supports it (the regex atsrc/thinking_parser.py:22matches<thinking>and<thought>; the screenshot shows the<think>format which is not currently supported — this is a separate bug and is deferred to the follow-up).Important scope clarification: The user's screenshot shows
<think>This is DWARF debug info...</think>style — using the half-width<think>(no closing match for the regex). The MiniMax fix in this track wraps the reasoning in<thinking>(the supported form), not<think>. This is a temporary scope reduction: the fix restores thinking-mono rendering for the common case (DeepSeek-style<thinking>tags), and the half-width<think>format is a known gap that's documented as a follow-up.
FR4: No new files in src/
Per the project's hard rule (see AGENTS.md "File Size and Naming Convention"), no new src/<thing>.py files. All fixes go in:
src/app_controller.py(FR1, FR2)src/ai_client.py(FR3)
FR5: Tests cover all 3 fixes
tests/test_ai_loop_regressions_20260614.py(new file): TDD tests for FR1, FR2, FR3.- FR1 tests (3+ tests): (a) successful response becomes a discussion entry; (b) error response becomes a discussion entry with
status="error"; (c)_ai_statusis updated correctly on both paths. - FR2 tests (2+ tests): (a) the dead
except ProviderErrorclause is removed (assert no longer present via AST scan); (b) the replaced code path correctly raises HTTPException for the API sites. - FR3 tests (2+ tests): (a)
_send_minimaxreturnsResult.datathat contains<thinking>tags when reasoning is extracted; (b) the discussion entry'sthinking_segmentsfield is populated whenparse_thinking_traceis run on the result.
- FR1 tests (3+ tests): (a) successful response becomes a discussion entry; (b) error response becomes a discussion entry with
5. Non-Functional Requirements
- NFR1 (Atomic per-task commits): each plan task is one commit; no batching.
- NFR2 (1-space indentation): enforced by the project's AI-Optimized Python style.
- NFR3 (No diagnostic noise in production): no
sys.stderr.write("[XYZ_DIAG] ...")lines in the committed code. If instrumentation is needed for the TDD test, it goes totests/artifacts/<test_name>.diag.log(not in the test file itself). - NFR4 (Backward compatibility): the deprecated
ai_client.send()shim remains working (thepublic_api_migration_20260606track is responsible for removal; this track only fixes the 3 broken except clauses). - NFR5 (No regression in other providers): the 5 unaffected providers (Anthropic, Qwen, Grok, Llama, Llama native) must continue to pass their existing tests.
- NFR6 (Thread safety): all fixes preserve the existing
_send_lockand per-provider history locks; the fix for FR1 must not introduce a new race between the streaming_on_ai_streamcallback and the finalresult.datawrite.
6. Architecture Reference
For implementation details, consult:
docs/guide_ai_client.md: the canonical guide forsrc/ai_client.py; the newsend_result()API is documented in the "Data-Oriented Error Handling (Fleury Pattern) > Public API" section. FR1 and FR3 should follow the patterns shown there.docs/guide_app_controller.md: the canonical guide forsrc/app_controller.py; the_handle_request_eventand_on_comms_entryflows are described in §"AI Loop Lifecycle". FR1 and FR2 changes are in this subsystem.docs/guide_thinking.md(if it exists; otherwisedocs/guide_discussions.md): the canonical guide for thinking-mono rendering; theparse_thinking_tracemarkers are documented in §"Thinking Markers".conductor/code_styleguides/error_handling.md: the canonical reference for the Result/ErrorInfo pattern; the new FR2 code paths should follow §3.1 "AND over OR (Result struct with side-channel errors)".docs/reports/data_oriented_error_handling_phase3_20260612.md(if it exists; otherwise the metadata.jsondeprecation_strategysection of the parent track): documents thesend_result()deprecation strategy and the plannedpublic_api_migration_20260606follow-up.
7. Out of Scope
- Gemini / Gemini CLI thinking-format compatibility investigation (Bug #4 / G4). The user's complaint includes Gemini, but the format may be a pre-existing limitation. Documented as a follow-up in §13.1.
- Migrating the remaining 5 production call sites + 63 test call sites to
send_result(). The plannedpublic_api_migration_20260606track handles this. - Expanding
thinking_parser.pyto support new marker formats (e.g.,<think>without closing</think>). - Restructuring
_handle_request_eventto be testable in isolation (a follow-up; this track's tests use mocks for the AI client, not the controller). - Any changes to the
multi_agent_conductor.pyMMA worker interface (it still usessend(); will migrate in the public_api track). - Restoring the
<think>(half-width) marker support. The user's screenshot shows this format; the currentparse_thinking_traceregex requires<thinking>(full-width). This is a separate gap documented in §13.2.
8. Phases (Summary)
| Phase | Name | Tasks | Verification |
|---|---|---|---|
| Phase 1 | Root-cause verification (TDD red) | 3 tasks: write 3+ failing tests for FR1, FR2, FR3; commit each as a separate test | pytest tests/test_ai_loop_regressions_20260614.py shows red |
| Phase 2 | Fix FR1 (Bug #2): error response becomes a discussion entry | 3 tasks: implement the fix in _handle_request_event; run the FR1 tests; commit |
pytest tests/test_ai_loop_regressions_20260614.py::test_*fr1* shows green |
| Phase 3 | Fix FR2 (Bug #1): replace dead except ProviderError clauses |
3 tasks: replace 3 sites; run the FR2 tests; commit | pytest tests/test_ai_loop_regressions_20260614.py::test_*fr2* shows green; AST scan shows no ProviderError references |
| Phase 4 | Fix FR3 (Bug #3): MiniMax thinking mono rendering | 3 tasks: wrap reasoning in <thinking> tags in _send_minimax (or in run_with_tool_loop); run the FR3 tests; commit |
pytest tests/test_ai_loop_regressions_20260614.py::test_*fr3* shows green |
| Phase 5 | Regression sweep + docs | 3 tasks: run full pytest tests/; add follow-up note to docs/guide_ai_client.md "See Also" section; commit |
Full suite green; doc note present |
9. Risk Analysis
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
R1: The Phase 4 fix (MiniMax thinking wrap) breaks the existing DeepSeek tests because both use run_with_tool_loop. |
Medium | High | Apply the wrap only when reasoning_extractor is set AND returns non-empty; preserve the DeepSeek-specific path (which already wraps). The fix is conditional on caps.reasoning, not universal. |
R2: The FR1 fix changes the streaming behavior — the streaming chunks go through _on_ai_stream (via stream_callback), and the final result.data is set after streaming completes. The fix must not break the existing streaming contract. |
Medium | High | The FR1 fix only changes the FINAL response comms entry (after send_result() returns). The streaming path is unchanged. Phase 2's test must include a streaming test to lock this. |
R3: The 3 sites in app_controller.py that have except ProviderError may have other callers depending on the exception behavior. |
Low | Medium | All 3 sites are in _handle_request_event (1 site) and 2 API hook endpoints (/api/v1/generate, /api/v1/generate_sync). The fix routes errors the same way the original code intended, just via Result.ok instead of ProviderError. |
R4: The parse_thinking_trace regex is greedy; wrapping thinking in <thinking> tags and then parsing it may produce nested segments. |
Low | Low | The regex at src/thinking_parser.py:9 is re.DOTALL | re.IGNORECASE and uses .*? (non-greedy). Nested <thinking> blocks would not match because the outer block consumes the inner; this is the same behavior DeepSeek has, and the existing tests pass for DeepSeek. |
| R5: The user is wrong about Gemini / Gemini CLI — those may not actually be broken. | Medium | Low | The deferred Phase-5-style follow-up will investigate empirically. The user's primary report was MiniMax; the other 3 are mentioned as "all regressed" but the fix for Bug #1 (dead except clauses) and Bug #2 (empty data) restores them all to working order. The thinking-mono issue is MiniMax-specific. |
10. Coordination with Pending Tracks
This track is independent (no blocked_by and no blocks in metadata.json). It does not depend on or block any active track.
However, it interacts with:
public_api_migration_20260606(planned, not yet specced): this track's FR1 fix to_handle_request_eventis a partial migration. The full migration (5 production + 63 test sites) is out of scope here; the follow-up track picks up where this leaves off. The two tracks share the same destination but this track fixes the user-blocking regressions first.data_oriented_error_handling_20260606(shipped 2026-06-12): this track is the user-facing bug-fix for the issues introduced by the parent track. It does not modify any of the 3 files the parent track touched (mcp_client.py,ai_client.py,rag_engine.py); it only modifiesapp_controller.py(the 1 file the parent track did NOT touch). The MiniMax fix touchesai_client.pyfor FR3 (1 file the parent touched).qwen_llama_grok_followup_20260611(archived 2026-06-11): no direct interaction, but the MiniMax fix in FR3 follows the same reasoning-extraction pattern that track introduced for the OpenAI-compatible providers.
11. Verification Criteria (definition of "done")
The track is complete when ALL of the following are true:
- All 3 phase 1-4 tests pass (
pytest tests/test_ai_loop_regressions_20260614.pyshows green). - Full test suite passes (
uv run pytest tests/shows green; no new failures). grep -rn "ProviderError" src/returns no matches.grep -rn "ai_client\.ProviderError" src/returns no matches.- Live GUI test: a MiniMax
M2.7request with reasoning returns a discussion entry that includes athinking_segmentsfield (use thelive_guifixture +ApiHookClient.get_value("disc_entries")). - Live GUI test: a MiniMax request that fails (e.g., invalid API key) returns a discussion entry with
status="error"and the error message in thecontentfield. - Live GUI test: a Gemini request that succeeds returns a discussion entry (verifies the FR1 fix doesn't break Gemini).
docs/guide_ai_client.md"See Also" section includes the 2 follow-up notes (§13.1 Gemini thinking investigation, §13.2<think>half-width marker support).metadata.jsonverification_criteriafield is updated to reflect completion.
12. Acceptance Test (the user can verify this themselves)
After this track ships, the user should be able to:
- Open Manual Slop with MiniMax as the active provider.
- Send a message that requires the AI to reason (e.g., "explain the structure of this function").
- Verify: the AI's response appears in the Discussion Hub without manually pressing the
Historybutton. - Verify: the response has a
Monologuecollapsible section showing the AI's thinking. - Trigger a failure (e.g., switch to an invalid MiniMax API key, then send a message).
- Verify: an error entry appears in the Discussion Hub with the error message.
Before this track ships, steps 3 and 4 fail (for MiniMax); step 6 fails (for all 4 affected providers).
13. See Also — Follow-up Notes
13.1 Gemini / Gemini CLI thinking-format compatibility (deferred)
The user's complaint includes Gemini and Gemini CLI. The likely cause is a format mismatch between what the Gemini SDK outputs and what parse_thinking_trace recognizes:
parse_thinking_trace(src/thinking_parser.py:9) matches<thinking>,<thought>, andThinking:prefix.- The Gemini SDK's
resp.textmay include thinking as plain prose or as*thinking aloud*markdown, depending on the SDK version and the model's prompt formatting.
This track fixes Bugs #1, #2, #3. The Gemini / Gemini CLI thinking-format issue is plausibly a pre-existing limitation (the existing tests for parse_thinking_trace show it doesn't match all Gemini output formats) rather than a new regression from the recent refactor.
Follow-up track (to be specced): investigate empirically by running a Gemini request that produces reasoning and inspecting the raw resp.text; add a normalization pass in _send_gemini* if needed.
13.2 <think> (half-width) marker support (deferred)
The user's screenshot 1 shows a discussion entry containing <think>This is DWARF debug info, not the actual disassembly...</think> — the half-width <think> form (no closing </think> in the regex). The current parse_thinking_trace regex (src/thinking_parser.py:9) requires the full <thinking> form. Some models (notably certain DeepSeek-R1 outputs and possibly the MiniMax M2.7 output) use the half-width <think> form.
Follow-up track (to be specced): extend parse_thinking_trace to support the half-width <think>...</think> form (the closing tag is the same). The change is small (~3 lines in src/thinking_parser.py:9); the test file is tests/test_thinking_trace.py (5+ existing tests for the full-width form).
13.3 Public API Result Migration (planned, separate)
The public_api_migration_20260606 follow-up (planned, not yet specced) will migrate the 5 remaining production call sites and 63 test call sites to send_result(). This track fixes the 3 sites in app_controller.py that are actively broken; the public_api track picks up from there.