feat: strip reasoning from protected-exempt historical messages (gated) - #370
Closed
ranxianglei wants to merge 4 commits into
Closed
feat: strip reasoning from protected-exempt historical messages (gated)#370ranxianglei wants to merge 4 commits into
ranxianglei wants to merge 4 commits into
Conversation
Request-time pass (stripProtectedReasoning) that reclaims the never-compressible reasoning floor: reasoning parts on compress/skill assistant messages in CLOSED historical turns are dropped before the request is sent, while the current (possibly-open) round is always preserved. - lib/messages/reasoning-strip.ts: new pass, 3 gates (turn-closure via getLastUserMessage, protected-tool selector, size threshold > 2048 chars) - lib/hooks.ts: wired after hideConsumedCompressCalls, guarded by kill-switch - lib/config.ts: compress.stripProtectedReasoning (bool, default true) + compress.stripProtectedReasoningThreshold (number, default 2048); excluded from CompressOverridableConfig (global-only) - lib/config-validation.ts: registered both keys in VALID_CONFIG_KEYS + validateConfigTypes - dcp.schema.json: schema properties + default - tests: +19 tests (unit + hook-level kill-switch, mutation-verified) No provider gate (owner decision: handle reactively). No persisted-state or internal-tag changes. 1096/1096 tests pass. Fixes #368
📦 Built Plugin ArtifactBranch: Option A — Install from npm PR tag (recommended)opencode plugin opencode-acp@pr-370 --globalEach push to this PR publishes a new version under the Option B — Install from GitHubopencode plugin "github:ranxianglei/opencode-acp#2026-09-07_strip-protected-reasoning" --globalOption C — Download artifact
tar xzf opencode-acp-pr370.tgz
cp -r package/dist ~/.cache/opencode/packages/opencode-acp@latest/node_modules/opencode-acp/dist
This comment is automatically updated on each push. |
This was referenced Sep 8, 2026
…ze (review #368) Per the 2026-09-08 review session (owner-approved), three changes: - Provider allowlist gate (fail-closed): stripProtectedReasoningProviders (default [anthropic, gemini], '*' = all, case-insensitive substring, entries trimmed; undefined/unmatched provider strips nothing). hooks.ts resolves the CURRENT request's provider via requestModel?.providerID ?? state.modelProviderID (requestModel hoisted from the last user message's info.model). - Session activation gate: stripProtectedReasoningMinMessages (default 100, integer >= 0; 0 = always) — short sessions keep a byte-stable prefix. - Threshold default 2048 -> 0 (size is cache-noise; activation gate is the cache lever). Tests: 1124 pass (was 1112). Mutations verified: provider gate (5 fails), activation gate (2), hook options dropped (2), hook fallbacks dropped (1). Review round 2: dual-agent APPROVE; findings addressed (trim, config validation incl. Number.isInteger, hook-fallback e2e, tight boundaries).
Adds the 4 new compress.* keys to CONFIGURATION.md / CONFIGURATION.zh-CN.md (reference sections) and README.md / README.zh-CN.md (example config blocks): stripProtectedReasoning, ...Threshold, ...Providers, ...MinMessages.
Owner
Author
|
Superseded: owner redesign — the allowlist/activation-gate approach is replaced by a clean reimplementation: nested |
This was referenced Sep 9, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
On long sessions,
reasoningparts that ride oncompress/skill-carrying assistant messages form a permanently-incompressible context floor. Protection is message-granular (lib/compress/protected-content.ts:202), so the whole message — reasoning included — is excluded from every compression and re-sent on every turn. Each compression round adds ~9 KB of unreclaimable reasoning; measured ~83.5% of the never-covered residual in a real long session (#368). It is a monotonic feedback loop that degrades long-session usability.Solution
A single request-time pass,
stripProtectedReasoning, that drops thereasoningparts from protected-exempt messages in closed historical turns before the request is sent. The current (possibly-open) round is always preserved. No persisted-state or internal-tag changes; no DB writes.Five gates (request-level gates 4–5 run first; all must hold to strip)
compress.stripProtectedReasoningProviders(default["anthropic","gemini"],"*"= all, case-insensitive substring). Fail-closed: unknown or undefined provider → nothing is stripped. Rationale: GPT-family gateways may reject requests whose historical thinking blocks are incomplete.compress.stripProtectedReasoningMinMessages(default100,0= always). Below it the pass is a byte-stable no-op, so short sessions never pay prefix-cache churn.< lastUserIndex(index ofgetLastUserMessage). The active round is never touched.part.tool∈config.compress.protectedTools(compress/skill).threshold(default 0 — review found per-message size is cache-noise: invalidation propagates from the first stripped message, so the activation gate is the real cache lever).Action:
msg.parts = parts.filter(p => p.type !== "reasoning")— drops reasoning only; the tool call + other parts are preserved (the live summary lives in the compress-call body, not the reasoning).Placement
Inserted after
hideConsumedCompressCallsand beforeassignMessageRefsinlib/hooks.ts— operates on the minimal surviving set (reasoning-only leftovers are already spliced there, since reasoning is "structural" perlib/compress/parts.ts:1).Config (additive, safe defaults)
compress.stripProtectedReasoning(bool, defaulttrue) — kill-switch.compress.stripProtectedReasoningThreshold(number, default0) — size threshold in chars.compress.stripProtectedReasoningProviders(string[], default["anthropic","gemini"];"*"= all; explicit[]= strip for no provider) — added by review.compress.stripProtectedReasoningMinMessages(number, default100;0= always) — added by review.VALID_CONFIG_KEYS+validateConfigTypes; excluded fromCompressOverridableConfig(global-only).Files
lib/messages/reasoning-strip.ts— the pass, now with 5 gates + optionaloptionsparam (omitted = ungated for pure-function callers).lib/hooks.ts— passesstate.modelProviderID+ config gates into the call.lib/config.ts— 4 config keys (interface + defaults + merge +CompressOverridableConfigexclusion).lib/config-validation.ts—VALID_CONFIG_KEYS+validateConfigTypes(incl. string[] element validation).dcp.schema.json— schema properties + defaults.tests/reasoning-strip.test.ts— 18 pass tests + 5 merge tests + 14 gate tests.tests/e2e-message-transform.test.ts— hook-level kill-switch, provider-gate (fail-closed), and activation-gate tests.devlog/2026-09-07_strip-protected-reasoning/— REQ / DESIGN / WORKLOG (review updates recorded).Testing
??fallbacks → 1 e2e test fails (each verified by temporarily mutating the code, then restoring).Review
requestModel?.providerID ?? state.modelProviderID, fixes undefined-provider on a fresh session's first request), allowlist entry trimming, 9 new config-validation tests (incl. fractionalminMessagesnow rejected —Number.isInteger), hook-fallback e2e test (mutation-verified), tight boundary + presence assertions. One pre-existing follow-up noted: e2ebuildConfigis structurally incomplete (tests are excluded from typecheck).Secondary findings (filed as separate issues)
acp_statusestimator excludes reasoning (lib/messages/inject/utils.ts:586) → Display/acp_statuscontext-usage estimator excludes reasoning tokens (undercounts real usage) #371.byMessageIdentries with emptiedactiveBlockIdsstay visible (lib/messages/prune.ts:60-66) → OrphanedbyMessageIdentries with emptiedactiveBlockIdsstay visible forever #372.rewriteCompressInputfull-consumption leak (lib/compress/hide-consumed.ts:42) →rewriteCompressInputfull-consumption leak: compress part left intact when all blocks consumed #373.Fixes #368