fix(provider): honor the host's tool set so compaction can summarize - #91
Merged
Conversation
opencode calls the model with `tools: {}` on a compaction/summary turn, and
the bundled ai-sdk converts an empty tool record to `options.tools ===
undefined`. The Cursor agent runs its own tools regardless of what the host
declared, and the provider forwarded that activity as provider-executed
`tool-call` / `tool-input-start` parts. opencode's SessionProcessor rejects
those on a summary turn:
case "tool-input-start":
case "tool-call":
if (assistantMessage.summary)
throw Error(`Tool call not allowed while generating summary: ${name}`)
so the turn hard-errored and the session could not be compacted at all.
A host that declared no tools cannot accept tool parts, so route those turns
through the existing `"reasoning"` tool-display path: Cursor's tool activity
is folded into reasoning text instead of crossing the tool-execution
boundary. Turns that do declare tools are untouched and still render
structured blocks.
The empty-array case matters as well as `undefined`: ai-sdk's early return
only covers a null tool set, so a non-empty `tools` filtered down by
`activeTools` arrives as `[]`.
Manual `/compact` has been affected all along. Auto-compaction became
reachable only in 0.7.1-next.0, because #89 published real per-model context
windows — before that opencode resolved `limit.context` to 0 for every Cursor
model, and a zero context limit structurally disables the auto-compaction
trigger.
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
Compaction fails on Cursor models:
A failed compaction hard-errors the turn, so an oversized session cannot be compacted at all — the user is stuck.
Root cause
Three verified links:
1. The guard is opencode's. From the opencode 1.18.11 binary (
SessionProcessor):2. opencode declares zero tools on that turn.
SessionCompaction.processbuilds the assistant message withsummary: trueand callsprocess({ ..., tools: {}, system: [] }). The bundled ai-sdk then drops the tool fields entirely — an empty tool record short-circuits beforeactiveToolsfiltering:So the provider receives
options.tools === undefined.3. The provider never looked.
grep -rn "\.tools\b|toolChoice|activeTools" src/returned nothing before this change. Every turn ran the Cursor agent with its full native toolset, andcursorEventsToStreammapped that activity to provider-executed parts in the default"blocks"mode. Thecursor_-prefixed generic name matches the reportedcursor_context-mode_ctx_searchexactly.Chain: opencode declares no tools → Cursor agent uses its own tools anyway → provider forwards them as
tool-input-start/tool-call→ opencode's summary guard throws → compaction fails.Why not restrict tools on the Cursor run instead
AgentOptions,LocalAgentOptions, andLocalSendOptionsin@cursor/sdk@1.0.24have no tool allowlist, denylist, or tool-disable field.mode: "plan"restricts writes but not reads/search. The onlytools?: string[]in the package is a message field, not a run control. Suppression has to happen at the mapping boundary this plugin owns.Fix
Honor the host's declared tool set. If opencode offered no tools, it cannot accept tool parts — so route that turn through the already existing and already tested
toolDisplay: "reasoning"path, which renders Cursor's activity as reasoning text.options.toolstoolDisplayblocks) — unchangedundefinedreasoningundefinedreasoningactiveToolsfilters everything out[]reasoningThe empty-array row matters: ai-sdk's early return only covers a null tool set, so a non-empty
toolsnarrowed byactiveToolsarrives as[].No new config knob, no env var, no changes to MCP forwarding, session pooling, or the skills mirror.
Why it surfaced now
Not a code regression in 0.7.1-next.0, but that release is what switched the failing path on. opencode merges the plugin's config-channel entry over its models.dev entry; Cursor models are absent from models.dev, so the fallback applies:
and auto-compaction is gated on exactly that value:
git show be7a36e^:src/model-discovery.ts | grep limit→ no matches. Pre-#89 the config channel carried nolimit, socontextresolved to0and auto-compaction could never fire for a Cursor model. #89 published real windows, so it now does.Manual
/compacthas no such gate and was affected all along.Compaction with an agent-backed provider
Worth recording, since Cursor keeps its own server-side conversation state: opencode-side compaction is genuinely effective here, and the existing pool design already produces the right lifecycle.
system: [], soclassifyTurnreturnsside-call→ fresh ephemeral agent, pool untouched.isStrictPrefixfails →divergence→resumeAgentIdis never set → a fresh agent is seeded with the compacted transcript and re-pooled.So the bloated Cursor agent is retired and context genuinely shrinks. The only thing that was broken is the tool-part leak on the summary turn.
Tests
507 → 514. Every new test was mutation-checked, not merely observed green:
this.config.toolDisplay→ the 2 no-tools regression tests fail.expected '' to contain 'context-mode_ctx_search'), confirming they test the fold rather than mere absence.|| tools.length === 0→ the empty-array unit test fails.Coverage: 4 unit tests for
effectiveToolDisplay; a no-toolsdoStreamemits none oftool-input-start/tool-input-delta/tool-input-end/tool-call/tool-resultand folds the activity into reasoning while the summary text still reaches the host; a with-toolsdoStreamstill emitstool-call(positive control against silently killing normal tool blocks); a no-toolsdoGeneratemirror.Verification
npx tsc --noEmitclean ·npx vitest run514 passed (35 files) ·npm run buildsucceeds.Not included
The
EXC_GUARD/ guarded-fd process kill observed shortly after a compaction is not addressed here and is not yet attributed to this plugin. A userland double-close cannot produce that signature — probed directly on Bun 1.3.5 and Node, where doublecloseon file, socket, and stdio fds all yieldEBADF, neverEXC_GUARD— so it originates in native code, and the crash frames are unsymbolized. Investigation and its defensive hardening are deliberately held back rather than shipped on a guess.