Skip to content

fix: local-model reliability — compaction hang, Qwen3 reasoning toggle, stuck-loop backstop#188

Merged
elkaix merged 5 commits into
mainfrom
fix/compaction-hang-reasoning-toggle-stuck-loop
Jul 1, 2026
Merged

fix: local-model reliability — compaction hang, Qwen3 reasoning toggle, stuck-loop backstop#188
elkaix merged 5 commits into
mainfrom
fix/compaction-hang-reasoning-toggle-stuck-loop

Conversation

@elkaix

@elkaix elkaix commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Summary

Three related fixes for local-model reliability issues reported against LM Studio-hosted models:

  • Compaction hang: context-compaction summarization had no output-length cap, so a slow/degenerate completion from a local model could run unbounded and hang the soul loop (observed as a stuck "Compacting..." / "Bloviating..." state). capped_chat_provider() now caps the summary call via the correct provider-specific kwarg. Also fixes ScriptedEchoChatProvider, which was missing with_generation_kwargs entirely and would have crashed once the cap was wired in.
  • Qwen3.x reasoning toggle: Qwen3.x models expose a binary enable_thinking chat-template toggle, not tiered reasoning_effort levels. Self-hosted openai_legacy endpoints (llama.cpp/vLLM/LM Studio) were silently promoting every configured effort level to full reasoning for these models. Detects Qwen3.x models (mirroring the existing Kimi/GLM special-casing) and sends chat_template_kwargs.enable_thinking instead.
  • Stuck-loop backstop: the existing max_consecutive_failures backstop only trips when every tool call in a batch reports is_error=True, so it can't catch a loop where a tool falsely reports success on a call that never made progress (observed: an agent burning 15 minutes / 142k tokens with no backstop firing). Adds max_consecutive_identical_calls (default 10) as a second, independent backstop tracked from the toolset's own identical-argument repeat streak rather than each call's reported success/failure.

Test plan

  • make check-pythinker-code — ruff/format/pyright/ty all pass
  • make check-pythinker-core — ruff/pyright pass (ty non-blocking, pre-existing SDK-stub noise unrelated to this change)
  • Full tests + tests_e2e suite green (6556 passed)
  • packages/pythinker-core doctest suite green (314 passed)
  • New tests: compaction output-cap, Qwen3 enable_thinking toggle, identical-call stuck-loop backstop (fires + disables-at-zero)

Summary by CodeRabbit

  • New Features

    • Added configurable backstop for repeated tool-call loops: conversations now stop after max_consecutive_identical_calls consecutive identical tool-call arguments (default: 10; 0 disables).
    • Improved Qwen3.x thinking-mode handling on self-hosted OpenAI-legacy routes by using the chat-template toggle.
    • Capped compaction summary generation output per attempt to prevent runaway retries.
    • Scripted echo provider now preserves its queued script behavior when using with_generation_kwargs(...).
  • Bug Fixes

    • Prevented stuck turns from continuing indefinitely even when repeated tool calls appear to succeed without progress.

elkaix added 3 commits June 30, 2026 21:23
Compaction summarization had no output-length limit, so a slow or
degenerate completion from a local model could run unbounded and hang
the soul loop indefinitely (observed as a stuck "Compacting..." /
"Bloviating..." state). Add capped_chat_provider() to cap the summary
call via the correct provider-specific kwarg (max_tokens or
max_output_tokens), and wire it into SimpleCompaction.

Also fixes ScriptedEchoChatProvider, which was missing
with_generation_kwargs entirely.
…ffort

Qwen3.x models expose a binary enable_thinking chat-template toggle,
not the tiered low/medium/high reasoning_effort values OpenAI/Anthropic
providers use. Self-hosted openai_legacy endpoints (llama.cpp/vLLM/LM
Studio) were rejecting the tiered value for these models and silently
promoting every configured effort level to full reasoning. Detect
Qwen3.x models (mirroring the existing Kimi/GLM special-casing) and
send chat_template_kwargs.enable_thinking instead.
The existing max_consecutive_failures backstop only trips when every
tool call in a batch reports is_error=True, so it can't catch a
degenerate loop where a tool falsely reports success on a call that
never made progress (observed: a stuck agent burning 15 minutes and
142k tokens of context with no backstop firing).

Add max_consecutive_identical_calls (default 10), tracked from the
toolset's own identical-argument repeat streak rather than each call's
reported success/failure, as a second independent backstop.
@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4817c427-e98d-4f8d-a949-0a4f1f563e86

📥 Commits

Reviewing files that changed from the base of the PR and between 4bfa6ce and d0fb831.

📒 Files selected for processing (1)
  • tests/core/test_compaction_overflow.py

📝 Walkthrough

Walkthrough

Adds a capped output-token helper for compaction, switches Qwen3.x openai_legacy thinking to a binary chat-template toggle, and introduces a consecutive-identical-tool-call backstop with config, toolset, runtime, tests, and changelog updates.

Changes

Compaction cap, Qwen3 toggle, and stuck backstop

Layer / File(s) Summary
with_generation_kwargs support across chat providers
packages/pythinker-core/src/pythinker_core/chat_provider/echo/scripted_echo.py, packages/pythinker-core/tests/test_scripted_echo_chat_provider.py, tests/core/test_notifications.py, tests/core/test_model_switch_carryover.py
ScriptedEchoChatProvider and test provider doubles gain with_generation_kwargs(**kwargs), enabling chained generation-kwarg overrides and test verification.
capped_chat_provider helper and compaction wiring
src/pythinker_code/llm.py, src/pythinker_code/soul/compaction.py, tests/core/test_compaction_overflow.py
capped_chat_provider selects max_output_tokens or max_tokens per provider and is used in _summarize_to_message with a new _COMPACTION_MAX_OUTPUT_TOKENS = 4000 constant.
Qwen3.x enable_thinking binary toggle
src/pythinker_code/llm.py, tests/core/test_openai_provider.py
_is_qwen3_model detects Qwen3.x models; create_llm sets extra_body.chat_template_kwargs.enable_thinking instead of tiered with_thinking for Qwen3.x on openai_legacy routes.
Consecutive identical tool-call stuck backstop
src/pythinker_code/config.py, src/pythinker_code/soul/toolset.py, src/pythinker_code/soul/pythinkersoul.py, tests/core/test_config.py, tests/core/test_pythinkersoul_stuck_loop.py
New LoopControl.max_consecutive_identical_calls config field, PythinkerToolset.consecutive_repeat_count property, generalized _stuck_summary_message, and a new _step branch that stops turns with stop_reason="stuck" after repeated identical tool calls.
Changelog
CHANGELOG.md
Documents the three behavior changes above under Unreleased.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SimpleCompaction
  participant capped_chat_provider
  participant ChatProvider

  SimpleCompaction->>capped_chat_provider: capped_chat_provider(llm, 4000)
  capped_chat_provider->>ChatProvider: with_generation_kwargs(max_tokens=4000)
  ChatProvider-->>SimpleCompaction: capped provider
  SimpleCompaction->>ChatProvider: generate() summary request
Loading
sequenceDiagram
  participant PythinkerSoul
  participant PythinkerToolset
  participant StepOutcome

  PythinkerSoul->>PythinkerToolset: consecutive_repeat_count
  PythinkerToolset-->>PythinkerSoul: streak length
  PythinkerSoul->>PythinkerSoul: compare to max_consecutive_identical_calls
  PythinkerSoul->>StepOutcome: stop_reason="stuck"
Loading

Possibly related PRs

Suggested labels: bug

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers the changes and tests, but it omits the required Related Issue section and the template's Checklist items. Add the template sections: Related Issue with Resolve #(issue_number), a Description section, and the full checklist entries.
Docstring Coverage ⚠️ Warning Docstring coverage is 26.92% which is insufficient. The required threshold is 70.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title uses a valid conventional-commit prefix and accurately summarizes the main reliability fixes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/compaction-hang-reasoning-toggle-stuck-loop

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Jul 1, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/pythinker_code/llm.py`:
- Around line 64-85: The kwarg override map in capped_chat_provider does not
handle the openai_codex provider type, so it falls back to max_tokens instead of
max_output_tokens and the cap can be ignored. Update
_MAX_OUTPUT_TOKENS_KWARG_OVERRIDES in llm.py to include the openai_codex key,
matching the OpenAIResponses-backed path used by create_llm, and add a test for
capped_chat_provider or test_compaction_caps_output_tokens that covers
provider_config.type == "openai_codex" to verify the capped kwarg is applied
correctly.

In `@tests/core/test_pythinkersoul_stuck_loop.py`:
- Around line 163-199: The `_make_soul_with_pythinker_toolset` helper duplicates
the full `Runtime(...)` rebuild already done in `_make_soul`; extract that
shared construction into a small helper (for example, a runtime-rebuild
function) and have both helpers call it. Keep the differing parts in
`_make_soul_with_pythinker_toolset` limited to the `PythinkerToolset`, `Agent`,
and `PythinkerSoul` setup so the duplicated runtime field wiring is centralized.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3257809c-88d3-4b09-816e-e97f27dbbc9d

📥 Commits

Reviewing files that changed from the base of the PR and between b793056 and cc53fbd.

📒 Files selected for processing (14)
  • CHANGELOG.md
  • packages/pythinker-core/src/pythinker_core/chat_provider/echo/scripted_echo.py
  • packages/pythinker-core/tests/test_scripted_echo_chat_provider.py
  • src/pythinker_code/config.py
  • src/pythinker_code/llm.py
  • src/pythinker_code/soul/compaction.py
  • src/pythinker_code/soul/pythinkersoul.py
  • src/pythinker_code/soul/toolset.py
  • tests/core/test_compaction_overflow.py
  • tests/core/test_config.py
  • tests/core/test_model_switch_carryover.py
  • tests/core/test_notifications.py
  • tests/core/test_openai_provider.py
  • tests/core/test_pythinkersoul_stuck_loop.py

Comment thread src/pythinker_code/llm.py
Comment thread tests/core/test_pythinkersoul_stuck_loop.py
- Add openai_codex to the max_output_tokens kwarg-override map: it
  builds the same OpenAIResponses provider as openai_responses, so the
  compaction cap was silently no-op'ing (falling back to max_tokens)
  for ChatGPT/Codex-backed sessions.
- Add direct capped_chat_provider coverage for all provider-type ->
  kwarg mappings, including the openai_codex case above.
- Extract the duplicated Runtime(...) rebuild in
  test_pythinkersoul_stuck_loop.py into a shared helper.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/core/test_compaction_overflow.py`:
- Around line 75-82: The test currently ignores the return value from
capped_chat_provider() and only verifies the fake mutates in place, which
couples it to _FakeChatProvider.with_generation_kwargs() instead of the helper
contract. Update test_capped_chat_provider_picks_kwarg_by_provider_type to have
_FakeChatProvider.with_generation_kwargs() return a fresh provider instance,
capture the result of capped_chat_provider(), and assert on that returned
provider’s generation_kwargs rather than the original fake.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 831f1b8c-c2e5-46ef-956b-bcfeb5ca1c83

📥 Commits

Reviewing files that changed from the base of the PR and between cc53fbd and 4bfa6ce.

📒 Files selected for processing (3)
  • src/pythinker_code/llm.py
  • tests/core/test_compaction_overflow.py
  • tests/core/test_pythinkersoul_stuck_loop.py

Comment thread tests/core/test_compaction_overflow.py Outdated
_FakeChatProvider.with_generation_kwargs mutated self and returned self,
so the new provider-type-mapping test only observed the fake's internal
state rather than capped_chat_provider's actual return value -- a
regression that discarded the capped copy would have passed unnoticed.
Make the fake return a fresh instance (matching the real providers'
copy-on-write contract) and assert on the returned/used provider in
both the mapping test and test_compaction_caps_output_tokens.
@elkaix elkaix merged commit 840e9b9 into main Jul 1, 2026
42 checks passed
@elkaix elkaix deleted the fix/compaction-hang-reasoning-toggle-stuck-loop branch July 1, 2026 02:53
@elkaix elkaix mentioned this pull request Jul 1, 2026
9 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant