Skip to content

fix(agent,provider): un-stick compaction on thinking-model summaries and unnumbered overflows / 修复思考型模型摘要与无数字超窗导致的分段压缩死锁 - #9882

Merged
SivanCola merged 2 commits into
esengine:main-v2from
Linearl:fix/compact-thinking-summary-glm-overflow
Sep 7, 2026
Merged

fix(agent,provider): un-stick compaction on thinking-model summaries and unnumbered overflows / 修复思考型模型摘要与无数字超窗导致的分段压缩死锁#9882
SivanCola merged 2 commits into
esengine:main-v2from
Linearl:fix/compact-thinking-summary-glm-overflow

Conversation

@Linearl

@Linearl Linearl commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

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:

  1. Thinking-model summaries were misread as empty (internal/agent/compact.go): summarize() accumulated only ChunkText; DeepSeek vision thinking models put the whole briefing in reasoning_content with an empty content block, so every chunked fragment died on summarizer returned empty output (observed: fragment 2/14). Now a pure reasoning-only summary is surfaced (clamped to 32 KiB, matching the summaryOutputMaxTokens envelope), 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 in TestSummaryCollectorRejectsEmptyAndLengthLimitedOutput.
  2. Unnumbered provider overflows were invisible to the chunked fallback (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 and contextLimitInvariant requires window > 0, so AsContextLimitError returned 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. ParseContextLimitError now 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" (learnContextBudget only stores window > 0).

Testing

  • go test ./internal/provider/ -run TestParseContextLimit -count=1 — includes new TestParseContextLimitErrorGLMUnnumbered1261 (trusted overflow with zero token fields, case-insensitive message variant, 401 still rejected).
  • go test ./internal/agent/ -run 'TestSummaryCollector|TestSummarizer|TestOverflow' -count=1 — includes new TestSummarizerReasoningOnlyIsSurfacedNotEmptied; existing empty-output shape intent preserved.
  • Live validation: 2,032,885-token session no longer deadlocks on pressure compaction with both failure shapes active.

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:

  • Recovery: an overflow without token numbers no longer resends the same prompt under a clipped max_tokens; recoverContextLimit treats it as physically over the window and goes straight to overflow compaction (TestUnnumberedContextLimitSkipsIdenticalRetry).
  • Error text: a zero-field limit falls through to the generic 400 message with the provider's own reason instead of "prompt 0 + completion 0 = 0 tokens, window 0".
  • Reasoning clamp cuts on rune boundaries (truncateUTF8Bytes) and ChunkToolCallStart counts as a tool attempt (Copilot findings), each with a regression test.
  • Three comment blocks trimmed to repolint's limit so make lint passes.

Correction to the mechanism note above: on current main-v2 the chunked-fallback gates key only on errSummaryOutputTruncated / 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.

…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.
Copilot AI lite review requested due to automatic review settings September 7, 2026 06:26
@github-actions github-actions Bot added agent Core agent loop (internal/agent, internal/control) provider Model providers & selection (internal/provider) v2 Go rewrite (1.x) — main-v2 branch, active development labels Sep 7, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 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.

Comment thread internal/agent/compact.go Outdated
Comment thread internal/agent/compact.go Outdated
…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.
@SivanCola
SivanCola merged commit 6361ba2 into esengine:main-v2 Sep 7, 2026
27 checks passed
SivanCola added a commit to SivanCola/DeepSeek-Reasonix that referenced this pull request Sep 7, 2026
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.
Linearl added a commit to Linearl/DeepSeek-Reasonix that referenced this pull request Sep 8, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent Core agent loop (internal/agent, internal/control) provider Model providers & selection (internal/provider) v2 Go rewrite (1.x) — main-v2 branch, active development

Projects

None yet

3 participants