fix(agent,provider): un-stick compaction on thinking-model summaries and unnumbered overflows / 修复思考型模型摘要与无数字超窗导致的分段压缩死锁 - #9882
Conversation
…and unnumbered overflows Two compaction failure shapes observed on a 2M-token session (fork开发), where pressure-triggered summary retries never converged and the turn died with an HTTP 400 overflow: - summarize() accumulated only ChunkText, so a thinking-model summary (DeepSeek vision SKUs put the whole briefing in reasoning_content with an empty content block) returned 'summarizer returned empty output' and the chunked fragment fallback died on the same check forever (observed: fragment 2/14). Surface a pure reasoning-only summary (clamped), mirroring the boundedllm esengine#9679 treatment; a turn that also attempted tool calls keeps the empty-output rejection — that reasoning is private chain-of-thought, not digest material. - ParseContextLimitError could not recognize provider overflows that carry no token numbers: Zhipu GLM 1261 'Prompt exceeds max length' matched none of the numeric regexes and no JSON token field, so AsContextLimitError returned nil, the chunked compaction fallback never triggered, and the uncapped request failed transparently on every retry. Trust the provider-confirmed overflow with an unknown window (zero token fields); consumers already treat 0 as 'learn nothing, fall back to the configured window'. Guards: TestSummarizerReasoningOnlyIsSurfacedNotEmptied, TestParseContextLimitErrorGLMUnnumbered1261; existing TestSummaryCollectorRejectsEmptyAndLengthLimitedOutput shape intent (reasoning + tool call = reject) preserved.
There was a problem hiding this comment.
🟡 Changes recommended
summarize() currently (a) may mis-detect tool-call attempts because it only counts ChunkToolCall (not start/args-delta), and (b) clamps reasoning by raw byte slicing which can produce invalid UTF-8.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR fixes two compaction deadlock / overflow-recognition failure modes seen in very long sessions: (1) thinking-model summaries where the “answer” is emitted only via reasoning_content (empty text), and (2) provider context-limit overflows that don’t include any token counts (GLM 1261 “Prompt exceeds max length”), preventing chunked fallback from triggering.
Changes:
- Treat “unnumbered” provider-confirmed overflows as
ContextLimitError(window unknown / zero token fields) so overflow recovery and chunked compaction fallback can engage. - Surface reasoning-only summaries for compaction when text is empty and no tool call was attempted, with a 32KiB clamp.
- Add focused regression tests for both scenarios.
File summaries
| File | Description |
|---|---|
| internal/provider/context_limit.go | Recognizes GLM-style unnumbered overflows as trusted context-limit errors. |
| internal/provider/context_limit_test.go | Adds coverage for GLM unnumbered 1261 overflow parsing and status gating. |
| internal/agent/compact.go | Accumulates reasoning chunks and conditionally surfaces reasoning-only summary output (clamped). |
| internal/agent/compact_test.go | Extends fake provider to simulate reasoning-only streaming shape for tests. |
| internal/agent/compact_summary_failure_test.go | Adds regression ensuring reasoning-only summaries can install a projection instead of retry-looping on “empty output”. |
Review details
- Files reviewed: 5/5 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…asoning summaries Problem: an overflow parsed without token numbers (Zhipu GLM 1261) reached the sampling recovery path, which answered it by resending the same prompt under a clipped output cap and then failed; the user-facing error quoted "prompt 0 + completion 0 = 0 tokens, window 0". The reasoning-only summary clamp sliced bytes and could cut a multi-byte rune, an opened-but-unfinished tool call did not count as a tool attempt, and three comment blocks failed repolint's essay rule. Root cause: consumers of ContextLimitError assumed positive token fields, and the new clamp used a byte slice instead of the package's rune-safe helper. Fix: recovery treats a zero-field overflow as physically over the window and goes straight to overflow compaction; the error message falls through to the generic 400 text when no numbers are known; the clamp uses truncateUTF8Bytes; ChunkToolCallStart counts as a tool attempt; comments trimmed to the limit. Verification: go vet, repolint, golangci-lint clean; go test ./internal/agent ./internal/provider/... ./internal/control pass, including new TestUnnumberedContextLimitSkipsIdenticalRetry, TestSummarizerReasoningWithToolCallStaysEmpty, TestSummarizerReasoningClampKeepsValidUTF8, and the zero-field errmsg case.
Integrate the reviewed context-overflow recovery changes from esengine#9882 and esengine#9879. Preserve the kernel-reduced startup graph rather than restoring the pre-kernel raw budget. The integrated build measures 2,438,339 raw bytes (2381.190 KiB), 186 bytes above the repaired PR head. Set only the raw ceiling to 2381.2 KiB; all gzip, CSS, locale and largest-chunk gates remain unchanged. Validation: frontend build, test typecheck, context-maintenance notice tests, agent/control/provider/boot Go tests and repolint passed. Transcript measurement and session-command production repairs are unchanged.
Fork-side resolutions: - compact/session_extract: take upstream (fork fix already landed via esengine#9882 MERGED); esengine#9885 parallelization stays on its PR branch for rebase - esengine#9693 family (AskCard/useController/locales/turnSubmissionFailure): take upstream native (fork cherry-pick was equivalent) - context_limit: take upstream (esengine#9882 GLM 1261 recognition included) - session_catalog_targets: keep fork case-fold dedup (pre-path_identity guard against duplicate scans) - i18n x3 + slash_registry: union (upstream work-mode copy + fork subagent-policy keys) - eventwire/wire.go: upstream field skeleton + fork TokensPerSec - serve.go: upstream mirror fields + fork GrandCouncil heartbeat fields - controller.go: fork MemorySystemReload + upstream capability resolver - settings_app.go: upstream provider fields + fork HighSpeedModels - tabs.go: upstream sessionGeneration/takeoverMirror + fork pendingRuntimeEvents (esengine#9601) + 3-value desktopNewSessionDefaults - topic_archive_test.go: keep both test functions - evidence/classify_profile.go: drop auto-merge duplicate case - ProjectTree: catalogPartial union (repairPending + repairActive) Known follow-ups (tracked in task 11): SettingsPanel taken wholesale from upstream — fork-only settings UI (vision/high-speed models, subagent tier) needs a follow-up port; 113 tsc errors at merge time reduced to 2 (bridge marker + tabs field), both fixed here.
Summary
Fixes #9878 — two compaction failure shapes observed on a real 2M-token session, where pressure-triggered summary retries never converged and the turn died with an HTTP 400 overflow:
internal/agent/compact.go):summarize()accumulated onlyChunkText; DeepSeek vision thinking models put the whole briefing inreasoning_contentwith an empty content block, so every chunked fragment died onsummarizer returned empty output(observed:fragment 2/14). Now a pure reasoning-only summary is surfaced (clamped to 32 KiB, matching thesummaryOutputMaxTokensenvelope), mirroring the boundedllm treatment in fix(goaleval): parse reasoning-only evaluator responses / 修复Goal评估器在思考型模型下空响应致目标暂停 #9679. A reasoning + tool-call turn keeps the empty-output rejection — that reasoning is private chain-of-thought, not digest material, preserving the shape contract inTestSummaryCollectorRejectsEmptyAndLengthLimitedOutput.internal/provider/context_limit.go): Zhipu GLM reports{"error":{"code":"1261","message":"Prompt exceeds max length"}}with no token numbers — none of the numeric regexes match andcontextLimitInvariantrequireswindow > 0, soAsContextLimitErrorreturned nil and the chunked fallback ([Feature]: Extract key points from an over-length/unusable legacy session into a fresh conversation / 对超长/无法继续的旧会话:一键结构化提取要点并生成新会话 #9082 family) never triggered; the uncapped fold then failed transparently on every retry, defeating the [Bug]: Compaction cannot rebuild a folded projection once invalidated — uncapped summary requests overflow shared-window models / 投影失效后压缩无法重建折叠投影:无安全前缀上限的摘要请求必然溢出共享窗口模型 #9572 un-deadlock handoff.ParseContextLimitErrornow trusts the provider-confirmed overflow with zero token fields (window unknown); consumers already treat window=0 as "learn nothing, fall back to the configured window" (learnContextBudgetonly storeswindow > 0).Testing
go test ./internal/provider/ -run TestParseContextLimit -count=1— includes newTestParseContextLimitErrorGLMUnnumbered1261(trusted overflow with zero token fields, case-insensitive message variant, 401 still rejected).go test ./internal/agent/ -run 'TestSummaryCollector|TestSummarizer|TestOverflow' -count=1— includes newTestSummarizerReasoningOnlyIsSurfacedNotEmptied; existing empty-output shape intent preserved.Cache Impact / 缓存影响
Cache-impact: none - summarize surface and context-limit parsing do not touch promptCacheKey, projection keys, or cache-key derivation; reasoning-only surfacing feeds the compaction digest path only. / 仅压缩摘要与超窗识别路径,不触及 promptCacheKey/会话投影/缓存键逻辑。
Cache-guard:
go test ./internal/agent/ -run 'TestSummaryCollector|TestSummarizer' -count=1 && go test ./internal/provider/ -run TestParseContextLimit -count=1/ 两条路径均有定点回归守护。Documentation-impact: none - internal-only fix, no docs or config surface changed.
System-prompt-review: none - no system prompt text changes.
Refs: #9878, #9678/#9679 (same reasoning-only shape, boundedllm side), #9082, #9572
Review fixes (41594e9) / 评审修复
Pushed by the maintainer on top of the original commit:
max_tokens;recoverContextLimittreats it as physically over the window and goes straight to overflow compaction (TestUnnumberedContextLimitSkipsIdenticalRetry).truncateUTF8Bytes) andChunkToolCallStartcounts as a tool attempt (Copilot findings), each with a regression test.make lintpasses.Correction to the mechanism note above: on current
main-v2the chunked-fallback gates key only onerrSummaryOutputTruncated/ErrCompactionRequired, so recognizing the GLM overflow does not by itself route the summary path into chunked recovery; that wiring lands with #9879, whose ladder skips the re-plan for zero-field limits and goes to the transcript form, then the fragment path. This PR is independently valuable regardless: reasoning-only summaries no longer dead-loop, and the sampling path now recovers a GLM overflow through compaction instead of an identical retry.